SpringBoot构建电商基础秒杀项目(二)
第四章 商品模块开发
4.1 商品模型管理——商品创建
上来第一步不是根据产品经理给的需求建数据库、表。
而是先进行领域模型设计,再建库、表。
比如,先想好,密码和用户信息应该分开存放,然后再去建表。
也就是说,先思考设计Model,再去动手建表。
https://blog.csdn.net/midnight_time/article/details/98726093
https://blog.csdn.net/m0_37657841/article/details/90545998
1.首先设计商品领域模型
新建ItemModel,设置属性们:商品名称,价格,库存,描述信息,销量...
2.设计数据库
新建两张表:商品表和库存表 外键为item_id(这个好像不写也行??因为商品的id会自增,model转库存得到数据库结果之前,会先拿商品,找商品id拿到再传给库存。
数据库创建还报错,除了id默认值都写了,检查好久发现double类型的长度不能写10.0要写成10,0(用逗号!!!)
ps:数据库里我们price用的double类型,但是model里price用的是java.math.BigDecimal类型
注意,Model中price是BigDecimal类型,而DO中price是Double类型
这样会导致copy忽略price
所以,copy之后,还需要手动转换price的类型,
再手动set到dataobject中
一般情况下,对于那些不需要准确计算精度的数字,我们可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以开发中,如果我们需要精确计算的结果,则必须使用BigDecimal类来操作。
pss:BigDecimal格式化保留2为小数,不足则补0:https://www.cnblogs.com/zhangyinhua/p/11545305.html
public class NumberFormat { public static void main(String[] s){ System.out.println(formatToNumber(new BigDecimal("3.435"))); System.out.println(formatToNumber(new BigDecimal(0))); System.out.println(formatToNumber(new BigDecimal("0.00"))); System.out.println(formatToNumber(new BigDecimal("0.001"))); System.out.println(formatToNumber(new BigDecimal("0.006"))); System.out.println(formatToNumber(new BigDecimal("0.206"))); } /** * @desc 1.0~1之间的BigDecimal小数,格式化后失去前面的0,则前面直接加上0。 * 2.传入的参数等于0,则直接返回字符串"0.00" * 3.大于1的小数,直接格式化返回字符串 * @param obj传入的小数 * @return */ public static String formatToNumber(BigDecimal obj) { DecimalFormat df = new DecimalFormat("#.00"); if(obj.compareTo(BigDecimal.ZERO)==0) { return "0.00"; }else if(obj.compareTo(BigDecimal.ZERO)>0&&obj.compareTo(new BigDecimal(1))<0){ return "0"+df.format(obj).toString(); }else { return df.format(obj).toString(); } } } 结果为: 3.44 0.00 0.00 0.00 0.01 0.21
3.修改pom文件不需要自动覆盖了,原来的true改为false
4.修改mybatis-generator配置文件添加两张表生成相关
就改一下之前的表名包名就可以,然后运行mvn mybatis-generator:generate
自动生成。数据类型错了这里可以删除这些生成的文件改变想改的数据库重新生成就是!!!!!前面一章那个说不定有救。。。
然后老师说这个price是double类型,待会需要手动转成bigdecimel类型。。
(前面一章是不是那个gender类型错误所以出错?不是,改了也一样。然后重新生成也没用,救不活。。。)
5.修改mapper的xml文件
把insert和insertSelective方法后添加属性 keyProperty="id" useGeneratedKeys="true",使其保持自增
6.创建ItemService接口
public interface ItemService { //创建商品 ItemModel createItem(ItemModel itemModel); //商品列表浏览 List<ItemModel> listItem(); //商品详情浏览 ItemModel getItemById(Integer id); }
创建商品方法其实就是和注册用户差不多流程,就是可以方便地在前端设置商品的属性存入数据库。
list方法是展示所有商品用的。
getbyid方法是展示某个商品详情需要用到的。
7.ItemServiceImpl实现类(别忘了类上加@Service
入参校验后的ItemMoel(就是设置@notblank@notnull这些
public class ItemModel { private Integer id; @NotBlank(message = "商品名称不能为空") private String title; @NotNull(message = "商品价格不能为空") @Min(value = 0, message = "商品价格必须大于0") private BigDecimal price; @NotNull(message = "库存必须填写") private Integer stock; @NotNull(message = "商品描述不能为空") private String description; /** * 销量 */ private Integer sales; @NotNull(message = "商品图片信息不能为空") private String imgUrl; /** * 使用聚合模型,如果promoModel不为空,则表示其拥有还未结束的秒杀活动 */ private PromoModel promoModel; public PromoModel getPromoModel() { return promoModel; } public void setPromoModel(PromoModel promoModel) { this.promoModel = promoModel; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public BigDecimal getPrice() { return price; } @Override public String toString() { return "ItemModel{" + "id=" + id + ", title='" + title + '\'' + ", price=" + price + ", stock=" + stock + ", description='" + description + '\'' + ", sales=" + sales + ", imgUrl='" + imgUrl + '\'' + ", promoModel=" + promoModel + '}'; } public void setPrice(BigDecimal price) { this.price = price; } public Integer getStock() { return stock; } public void setStock(Integer stock) { this.stock = stock; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Integer getSales() { return sales; } public void setSales(Integer sales) { this.sales = sales; } public String getImgUrl() { return imgUrl; } public void setImgUrl(String imgUrl) { this.imgUrl = imgUrl; } }
实现方法
/** * @param itemModel:商品模型 * @return ItemModel:商品模型 给上层看 其实主要是入库 * @description 创建商品! */ @Override @Transactional public ItemModel createItem(ItemModel itemModel) throws BusinessException { //校验入参 ValidationResult result = validator.validate(itemModel); if (result.isHasErrors()) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, result.getErrMsg()); } //转化model Item item = this.convertItemFromItemModel(itemModel); //写入数据库!! itemMapper.insertSelective(item);
itemModel.setId(item.getId());//用来绑的库存表的itemid
ItemStock itemStock = this.convertItemStockFromItemModel(itemModel); itemStockMapper.insertSelective(itemStock); //返回对象 return this.getItemById(itemModel.getId()); }
/** * @param itemModel * @return Item * @description 将itemModel转换为Item!!!!大转小!小是用来操作数据库的 */ private Item convertItemFromItemModel(ItemModel itemModel) { if (itemModel == null) { return null; } Item item = new Item(); BeanUtils.copyProperties(itemModel, item); //数据库中price是double类型的,ItemModel中是BigDecimal,因为要避免类型转化时出现精度丢失,前端不能用double item.setPrice(itemModel.getPrice().doubleValue());//这里是因为类型不一样所以需要手动设置!!! return item; }
/**
* @param itemModel
* @return ItemStock:商品库存
* @description 将itemModel转化为ItemStock!大转小
*/
private ItemStockDO convertItemStockFromItemModel(ItemModel itemModel) {
if (itemModel == null) {
return null;
}
ItemStockDO itemStock = new ItemStockDO();
itemStock.setItemId(itemModel.getId());//存入itemid
itemStock.setStock(itemModel.getStock());//也是set,和密码一样
return itemStock;
}
小转大聚合成Model 返回给上一层时用 (一般vo层最大)
/** * @param item * @param itemStock * @return ItemModel * @description 将item和itemStock转换为ItemModel!小转大! */ private ItemModel convertFromEntity(Item item, ItemStock itemStock) { if (item == null) { return null; } ItemModel itemModel = new ItemModel(); BeanUtils.copyProperties(item, itemModel); //从数据库data向item model转的时候也要注意price的类型问题!!! itemModel.setPrice(new BigDecimal(item.getPrice())); itemModel.setStock(itemStock.getStock()); return itemModel; }
8.ItemController
新建ItemVO
其实这里跟model暂时没区别
public class ItemVO { private Integer id; private String title; private BigDecimal price; private Integer stock; private String description; private Integer sales; private String imgUrl; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } public Integer getStock() { return stock; } public void setStock(Integer stock) { this.stock = stock; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public Integer getSales() { return sales; } public void setSales(Integer sales) { this.sales = sales; } public String getImgUrl() { return imgUrl; } public void setImgUrl(String imgUrl) { this.imgUrl = imgUrl; } }
controller中写对应方法:
/** * @param [ title:商品标题, price:价格, description:描述, imgUrl:图片链接, stock:库存] * @return CommonReturnType:通用返回类型 * @description 创建商品 */ @PostMapping(value = "/create", consumes = CONTENT_TYPE_FORMED) @ResponseBody public CommonReturnType createItem(@RequestParam(name = "title") String title, @RequestParam(name = "price") BigDecimal price, @RequestParam(name = "description") String description, @RequestParam(name = "imgUrl") String imgUrl, @RequestParam(name = "stock") Integer stock) throws BusinessException { ItemModel itemModel = new ItemModel(); itemModel.setTitle(title); itemModel.setPrice(price); itemModel.setDescription(description); itemModel.setImgUrl(imgUrl); itemModel.setStock(stock); ItemModel itemModelForReturn = itemService.createItem(itemModel); ItemVO itemVO = convertVOFromModel(itemModelForReturn); return CommonReturnType.create(itemVO); }
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1"> --> <link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <link href="static/assets/global/css/components.css" rel="stylesheet" type="text/css"/> <link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/> <script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script> </head> <body class="login"> <div class="content"> <h3 class="form-title">创建商品</h3> <div class="form-group"> <label class="control-label">商品名</label> <div> <input class="form-control" type="text" name="title" id="title"> </div> </div> <div class="form-group"> <label class="control-label">商品价格</label> <div> <input class="form-control" type="text" name="price" id="price"> </div> </div> <div class="form-group"> <label class="control-label">商品库存</label> <div> <input class="form-control" type="text" name="stock" id="stock"> </div> </div> <div class="form-group"> <label class="control-label">商品描述</label> <div> <input class="form-control" type="text" name="description" id="description"> </div> </div> <div class="form-group"> <label class="control-label">商品图片</label> <div> <input class="form-control" type="text" name="imgUrl" id="imgUrl"> </div> </div> <div class="form-actions"> <button class="btn blue" id="create" type="submit"> 提交创建 </button> </div> </div> </body> <script> $(document).ready(function() { $("#create").on("click", function() { var title = $("#title").val(); var price = $("#price").val(); var stock = $("#stock").val(); var description = $("#description").val(); var imgUrl = $("#imgUrl").val(); if (title == null || title == "") { alert("商品名不能为空"); return false; } if (price == null || price == "") { alert("商品价格不能为空"); return false; } if (stock == null || stock == "") { alert("商品库存不能为空"); return false; } if (description == null || description == "") { alert("商品描述不能为空"); return false; } if (imgUrl == null || imgUrl == "") { alert("商品图片不能为空"); return false; } $.ajax({ type: "POST", contentType: "application/x-www-form-urlencoded", url: "http://localhost:8081/item/create", data: { "title": title, "price": price, "stock": stock, "description": description, "imgUrl": imgUrl }, xhrFields:{ withCredentials:true }, success: function(data) { if (data.status == "success") { alert("创建成功"); } else { alert("创建失败,原因为" + data.data.errMsg); } }, error: function(data) { alert("创建失败,原因为" + data.responseText); } }); return false; }); }); </script> </html>
----------------------------------------------------------------------------------------------------------------------
我晕 又来未知错误。。。。
debug显示又是这儿插入数据库出错,和之前注册插入用户信息一模一样。。。。我之前换了别人的数据库创建语句也不行。。。
卧槽啥玩意儿啊 我把其他表也创好就好了??????mapper必须全?但我上一章也没有其他表啊。。。。
‘
这源码也看不懂啊。。。进去就是这个mapperproxy异常,总是抛出这个错。。。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } if (this.isDefaultMethod(method)) { return this.invokeDefaultMethod(proxy, method, args); } } catch (Throwable var5) { throw ExceptionUtil.unwrapThrowable(var5); }
Q:关于外键,好像数据库中设不设置都无所谓??而且实体类中根本都没有包含对方实体类型的引用????
A:是impl中大转小时设置了。然后方法中存商品数据后取id作为库存的itemid。
itemModel.setId(item.getId());//用来绑的库存表的itemid
UserPassword userPassword = new UserPassword();
userPassword.setEncrptPassword(userModel.getEncrptPassword());
userPassword.setUserId(userModel.getId());
return userPassword;
所以数据库外键暂时没啥用。。。
-----------------------------------------------------------------------------------------------------------------------------
9.商品详情页浏览
写一个get方法
impl:
/**
* @param id:商品id
* @return ItemModel
* @description 通过id获取商品详情!前面创建商品返回时用。要对应dao生成一下
*/
@Override
public ItemModel getItemById(Integer id) {
Item item = itemMapper.selectByPrimaryKey(id);
if (item == null) {
return null;
}
ItemStock itemStock = itemStockMapper.selectByItemId(item.getId());
//这里是用item的id关联到的itemid来查,如果没关联上,就查不到。
ItemModel itemModel = convertFromEntity(item, itemStock);
return itemModel;
}
这里获取商品详情用到库存的selectbyitemid需要手动添加一下。
controller:
@RequestMapping(value = "/get", method = {RequestMethod.GET}) @ResponseBody public CommonReturnType getItem(@RequestParam(name = "id") Integer id) { ItemModel itemModel = itemService.getItemById(id); ItemVO itemVO = convertVOFromModel(itemModel); return CommonReturnType.create(itemVO);
浏览器输入http:localhost:8081/item/get?id=15 (自己写的请求参数)
点击图片链接,由浏览器跳转到图片网址上
4.2 商品模型管理——商品列表
假设我们的需求是按照销量从高到低显示所有商品
1.创建sql语句
在ItemDOMapper.xml中新建方法
<select id="listItem" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from item </select>
这个base_column_list是一个sql语句,就是所有的列名
2.在ItemDOMapper中创建方法
List<ItemDO> listItem();
3.在ItemServiceImpl中实现把商品类用列表展示的方法!!!!!(没见过这操作,get了)
@Override public List<ItemModel> listItem() { List<ItemDO> itemDOList = itemDOMapper.listItem(); //使用Java8的stream API!!!!没用过。。。
//将里面的每一个item实体类和stock实体类map成itemModel,就是小copy进入大组成list。
重点是成为了list而不是小转大,小转大方法已经写过了!!!而且下面也用了见convert!!!!! List<ItemModel> itemModelList = itemDOList.stream().map(itemDO -> { ItemStockDO itemStockDO = itemStockDOMapper.selectByItemId(itemDO.getId()); ItemModel itemModel = this.convertModelFromDataObject(itemDO, itemStockDO); return itemModel; }).collect(Collectors.toList()); return itemModelList; }
4.controller层展示页面浏览 也用了steam().map
/** * @author Zhang Yifei * @date 2019/12/10 * @return CommonReturnType * @description 获取商品列表 */ @GetMapping(value = "/list") @ResponseBody public CommonReturnType listItem() {//这里没写对应要的参数,是因为全要了吗??不对啊mvcday1里括号都有注解。。。
//这儿应该是不需要拿参数,因为既不需要靠请参参数获取返回的数据,也不需要post数据。只是把数据库get返回到页面。去掉@requestbody也可以 List<ItemModel> itemModelList = itemService.listItem(); //使用stream api将list内的itemModel转化为itemVO的list List<ItemVO> itemVOList = itemModelList.stream().map(itemModel -> { ItemVO itemVO = this.convertVOFromModel(itemModel); return itemVO; }).collect(Collectors.toList()); return CommonReturnType.create(itemVOList); }
浏览器输入:http:localhost:8081/item/list
item表和item_stock表中数据全部列出
4.3 商品模型管理——商品列表页面
写前端展示数据库。(之前学web的时候有一个前端增删改列表的案例很像,那个案例还有分页展示)
用jquery的方式给表格里填充从数据库查询的数据(之前那个表格是jsp的el表达式和tstl好像
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1"> --> <link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <link href="static/assets/global/css/components.css" rel="stylesheet" type="text/css"/> <link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/> <script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script> </head> <body> <div class="content"> <h3 class="form-title">商品列表浏览</h3> <div class="table-responsive"> <table class="table"> <thead> <tr> <th>商品名</th> <th>商品图片</th> <th>商品描述</th> <th>商品价格</th> <th>商品库存</th> <th>商品销量</th> </tr> </thead> <tbody id="container"> </tbody> </table> </div> </div> </body> <script> // 定义全局商品数组信息 var g_itemList = []; $(document).ready(function() { $.ajax({ type: "GET", url: "http://localhost:8081/item/list", xhrFields:{ withCredentials:true, }, success: function(data) { if (data.status == "success") { g_itemList = data.data; reloadDom(); } else { alert("获取商品信息失败,原因为" + data.data.errMsg); } }, error: function(data) { alert("获取商品信息失败,原因为" + data.responseText); } }); }); function reloadDom() { for (var i = 0; i < g_itemList.length; i++) { var itemVO =g_itemList[i]; var dom = "<tr data-id='"+itemVO.id+"' id='itemDetail"+itemVO.id+"'>\ <td>"+itemVO.title+"</td>\ <td><img style='width:100px;heigth:auto;' src='"+itemVO.imgUrl+"'/></td>\ <td>"+itemVO.description+"</td>\ <td>"+itemVO.price+"</td>\ <td>"+itemVO.stock+"</td>\ <td>"+itemVO.sales+"</td>\ </tr>"; $("#container").append($(dom));//使用jquery的方法往id为container的元素中填充dom //点击一行任意的位置 跳转到商品详情页 $("#itemDetail"+itemVO.id).on("click", function(e) { window.location.href="getitem.html?id="+$(this).data("id"); }); } } </script> </html>
数据库中图片的url可以用本地的
INSERT INTO `item`(`id`,`title`,`price`,`description`,`sales`,`img_url`) VALUES
(1,'iphone111',9999,'12345',103,'123.jpg');
然后这些图片得和html存一起就不用加地址
4.4 商品模型管理——商品详情页面
开始做点击上表任意一行,跳转到的商品详情页
写getItem.html
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1"> --> <link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <link href="static/assets/global/css/components.css" rel="stylesheet" type="text/css"/> <link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/> <script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script> </head> <body class="login"> <div class="content"> <h3 class="form-title">商品详情</h3> <div id="promoStartDateContainer" class="form-group"> <label style="color:blue" id="promoStatus" class="control-label"></label> <div> <label style="color:red" class="control-label" id="promoStartDate" /> </div> </div> <div class="form-group"> <div> <label class="control-label" id="title" /> </div> </div> <div class="form-group"> <div> <img style="width:200px;height:auto;" id="imgUrl"> </div> </div> <div class="form-group"> <label class="control-label">商品描述</label> <div> <label class="control-label" id="description" /> </div> </div> <div id="normalPriceContainer" class="form-group"> <label class="control-label">商品价格</label> <div> <label class="control-label" id="price" /> </div> </div> <div class="form-group"> <label class="control-label">商品库存</label> <div> <label class="control-label" id="stock" /> </div> </div> <div class="form-group"> <label class="control-label">商品销量</label> <div> <label class="control-label" id="sales" /> </div> </div> <div class="form-actions"> <button class="btn blue" id="createOrder" type="submit"> 立即购买 </button> </div> </div> </body> <script> var g_itemVO = {}; $(document).ready(function() { // 获取商品详情 $.ajax({ type: "GET", url: "http://localhost:8090/item/get", data: { "id": getParam("id"),//下面写的script方法 }, xhrFields:{ withCredentials:true }, success: function(data) { if (data.status == "success") { g_itemVO = data.data; reloadDom(); setInterval(reloadDom, 1000); } else { alert("获取信息失败,原因为" + data.data.errMsg); } }, error: function(data) { alert("获取信息失败,原因为" + data.responseText); } }); $("#createOrder").on("click", function() { $.ajax({ type: "POST", url: "http://localhost:8090/order/createorder", contentType: "application/x-www-form-urlencoded", data: { "itemId": g_itemVO.id, // "promoId": g_itemVO.promoId, "amount": 1, }, xhrFields:{ withCredentials:true }, success: function(data) { if (data.status == "success") { alert("下单成功"); window.location.reload(); } else { alert("下单失败,原因为" + data.data.errMsg + data.data.errCode); if (data.data.errCode == 20003) { window.location.href="login.html"; } } }, error: function(data) { alert("下单失败,原因为" + data.responseText); } }); }); }); function reloadDom() { $("#title").text(g_itemVO.title); $("#imgUrl").attr("src", g_itemVO.imgUrl); $("#description").text(g_itemVO.description); $("#price").text(g_itemVO.price); $("#stock").text(g_itemVO.stock); $("#sales").text(g_itemVO.sales); } function getParam(paramName) {//主要是由这个jquery的search方法解析url拿list.html传过来的 paramValue = "", isFound = !1; if (this.location.search.indexOf("?") == 0 && this.location.search.indexOf("=") > 1) { arrSource = unescape(this.location.search).substring(1, this.location.search.length).split("&"), i = 0; while (i < arrSource.length && !isFound) arrSource[i].indexOf("=") > 0 && arrSource[i].split("=")[0].toLowerCase() == paramName.toLowerCase() && (paramValue = arrSource[i].split("=")[1], isFound = !0), i++ } return paramValue == "" && (paramValue = null), paramValue } </script> </html>
主要是由这个jquery的search方法解析url拿list.html传过来的!!!重要操作
function getParam(paramName) { paramValue = "", isFound = !1; if (this.location.search.indexOf("?") == 0 && this.location.search.indexOf("=") > 1) { arrSource = unescape(this.location.search).substring(1, this.location.search.length).split("&"), i = 0; while (i < arrSource.length && !isFound) arrSource[i].indexOf("=") > 0 && arrSource[i].split("=")[0].toLowerCase() == paramName.toLowerCase() && (paramValue = arrSource[i].split("=")[1], isFound = !0), i++ } return paramValue == "" && (paramValue = null), paramValue }
完成了商品展示到详情页面,接下来还有交易改造写上。
第五章 交易模块开发
5.1 交易模型管理——交易模型创建
1.先设计用户下单的交易模型
也是先建模,再创表。
https://blog.csdn.net/m0_37657841/article/details/90637137
新建orderModel,添加属性比如交易号,交易号要用String类型
//用户下单的交易模型 public class OrderModel { //交易单号,例如2019052100001212,使用string类型 private String id; //购买的用户id private Integer userId; //购买的商品id private Integer itemId; //购买时商品的单价 private BigDecimal itemPrice; //购买数量 private Integer amount; //购买金额 private BigDecimal orderPrice; }
(后面应该还要增加notnull注解之类?不用了,这个不需要用户写,所以没有限制条件。这是数据库自己生成订单。
2.设计数据库
CREATE TABLE `order_info` (
`id` varchar(32) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,
`user_id` int(11) NOT NULL DEFAULT 0,
`item_id` int(11) NOT NULL DEFAULT 0,
`item_price` decimal(10, 2) NOT NULL DEFAULT 0.00,
`amount` int(11) NOT NULL DEFAULT 0,
`order_price` decimal(40, 2) NOT NULL DEFAULT 0.00,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
订单的id是varchar类型,是主键但没有设置自增。。
3.修改配置
<table tableName="order_info" domainObjectName="OrderDO" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" ></table>
4.生成文件(3个 金额数据库和生成的实体类是double,model里面是bigDecimal
在终端运行mvn mybatis-generator:generate
命令
5.2 交易模型管理——交易下单
1.OrderService接口写创建订单的方法,传入参数分别是用户id,商品id,商品数量
public interface OrderService { OrderModel createOrder(Integer userId, Integer itemId, Integer amount) throws BusinessException; }
OrderServiceImpl 实现类加@service
@Override @Transactional//方法加事务 public OrderModel createOrder(Integer userId, Integer itemId, Integer amount) throws BusinessException { //1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确 ItemModel itemModel = itemService.getItemById(itemId); if (itemModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在"); } UserModel userModel = userService.getUserById(userId); if (userModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息不存在"); } if (amount <= 0 || amount > 99) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不存在"); } //2.落单减库存 boolean result = itemService.decreaseStock(itemId, amount); if (!result) { throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH); } //3.订单入库 //4.返回前端 }
关于落单减库存方法
实际开发中库存应该单独一个服务器
因为库存是商品相关,加到商品业务层
3.ItemService接口增加decreaseStock方法
//库存扣减 boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException;
ItemServiceImpl
这里就用数据库操作后的返回条数判断就可以,不要用select for update语句,不然要做两次数据库操作,还得在jvm计算
@Override @Transactional public boolean decreaseStock(Integer itemId, Integer amount) throws BusinessException { int affectedRow = itemStockDOMapper.decreaseStock(itemId, amount); if (affectedRow > 0) { //更新库存成功 return true; } else { //更新库存失败 return false; } }
商品库存表减库存xml的sql:(重要!!!!!!!!!!!!!!!!!!!!1
<update id="decreaseStock" >
update item_stock
set stock = stock - #{amount}
where item_id = #{itemId} and stock >= #{amount}
</update>
对应的ItemStockMapper.java
int decreaseStock(@Param("itemId") Integer itemId, @Param("amount") Integer amount);
@parm注解对应传入参数对应数据库的栏位(第一次见这样的更新数据库方法!!!!
ps:老师用的ctrl+n搜索类名,双击shift全局查找想要的方法或类
https://www.cnblogs.com/pretty-sunshine/p/9950541.html
4.生成交易流水号
新建一个sequence_info表
CREATE TABLE `sequence_info` ( `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL, `current_value` int(11) NOT NULL DEFAULT 0, `step` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`name`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
插入一条语句,用来生成当前流水号(呆会儿要按名称oeder_info查找 从0开始每次加1
INSERT INTO `sequence_info` VALUES ('order_info', 0, 1);
修改mybatis-generator
在终端运行mvn mybatis-generator:generate
命令,生成3个文件。squence_info
修改SequenceDOMapper.xml
这里用的就是select for update的方式,可以上锁,因为获取以后就是要更新!!
<select id="getSequenceByName" parameterType="java.lang.String" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from sequence_info where name = #{name,jdbcType=VARCHAR} for update </select>
sequencemapping.java中添加方法
SequenceDO getSequenceByName(String name);
回到orderimpl中写产生订单号的方法:
/**
* @return String:订单号
* @description 生成订单号(如果事务createorder下单如果回滚,该下单方法中获得流水号id回滚,使等到的id号可能再一次被使用)
*/
@Transactional(propagation = Propagation.REQUIRES_NEW) //注解的这个属性使这个方法一定会提交,不管外部成功与否,不用重复订单号
String generateOrderNo() { //订单有16位 StringBuilder stringBuilder = new StringBuilder(); //前8位为时间信息,年月日 LocalDateTime now = LocalDateTime.now(); String nowDate = now.format(DateTimeFormatter.ISO_DATE).replace("-", ""); stringBuilder.append(nowDate); //中间6位为自增序列 //获取当前sequence(从数据库拿了之前预设好的信息,信息中规定了步长,由拼接自增6位 int sequence = 0; SequenceDO sequenceDO = sequenceDOMapper.getSequenceByName("order_info"); sequence = sequenceDO.getCurrentValue(); sequenceDO.setCurrentValue(sequenceDO.getCurrentValue() + sequenceDO.getStep());//从0开始每次加1 sequenceDOMapper.updateByPrimaryKeySelective(sequenceDO); //拼接(小于6位长度时后面补0!!!!因为是字符串形式所以可以这么干!!!!!! String sequenceStr = String.valueOf(sequence); for (int i = 0; i < 6 - sequenceStr.length(); i++) { stringBuilder.append(0); } stringBuilder.append(sequenceStr); //最后两位为分库分表位,暂时不考虑 stringBuilder.append("00"); return stringBuilder.toString(); }
实际开发中应该考虑这6位满了的情况,要重新循环,所以应该设置起止值。
这个方法用在同类OrderServiceImpl之前没写完的creatOrder方法中
把createOrder方法补全:(这里面有两个错会造成空指针异常!!!。。。见下面
@Override @Transactional public OrderModel createOrder(Integer userId, Integer itemId, Integer amount) throws BusinessException { //1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确 ItemModel itemModel = itemService.getItemById(itemId); if (itemModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在"); } UserModel userModel = userService.getUserById(userId); if (userModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息不存在"); } if (amount <= 0 || amount > 99) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "数量信息不存在"); } //2.落单减库存 (需要商品减少,所以要去itemsevice里面写 boolean result = itemService.decreaseStock(itemId, amount); if (!result) { throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH); } //3.订单入库 OrderModel orderModel = new OrderModel(); orderModel.setUserId(userId); orderModel.setItemId(itemId); orderModel.setAmount(amount); orderModel.setOrderPrice(orderModel.getItemPrice().multiply(new BigDecimal(amount)));//单价乘以数量 //生成交易流水号(订单号) orderModel.setId(this.generateOrderNo()); OrderDO order = convertFromModel(orderModel);//这里需要写一个转换方法 orderDOMapper.insertSelective(order); //增加商品销量 itemService.increaseSales(itemId,amount); //返回前端 return orderModel; }
现在点商品详情页下单,若未登录要有跳转到登录
因为:
1.枚举类中加了这个错误
2.getitem.html中增加了这个判断
进入商品list页选一个进入详情页点下单:
进入下单成功后,数据库库存要减1,order表要增加一行数据,sequence表的value值从0更新为1
5.销量增加
itemDOMapper.xml
和库存减少一样的更新操作。这里是操作item表。(重要!!!!!!!!!!!!1
<update id="increaseSales"> update item set sales = sales+ #{amount} where id = #{id,jdbcType=INTEGER} </update>
itemDOMapper接口
int increaseSales(@Param("id") Integer id, @Param("amount") Integer amount);
ItemServiceImpl
@Override @Transactional public void increaseSales(Integer itemId, Integer amount) throws BusinessException { itemDOMapper.increaseSales(itemId,amount); }
6.最终的OrderServiceImpl下单实现
@Service public class OrderServiceImpl implements OrderService { @Autowired private ItemService itemService; @Autowired private UserService userService; @Autowired private OrderMapper orderMapper; @Autowired private SequenceMapper sequenceMapper; /** * @author Zhang Yifei * @date 2019/12/10 * @param userId 用户id * @param amount 数量 * @param itemId 商品id * @param promoId 秒杀id * @return OrderModel 订单模型 * @description 1、前端url上传递秒杀活动id,下单接口内校验对应id是否属于对应商品且活动已开始(使用) * 2、直接在下单接口内判断对应商品是否存在秒杀活动,若存在则以秒杀价格下单(会检验两次是否是秒杀商品) */ @Override @Transactional public OrderModel createOrder(Integer userId, Integer itemId,Integer promoId, Integer amount) throws BusinessException { //校验下单状态,商品是否存在,用户是否合法,数量是否正确,活动信息 ItemModel itemModel = itemService.getItemById(itemId); if (itemModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在"); } UserModel userModel = userService.getUserById(userId); if (userModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息不存在"); } if (amount <= 0 || amount > 99) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "购买数量不正确"); } if (promoId != null) { //校验活动是否存在适用商品 if (promoId.intValue() != itemModel.getPromoModel().getId()) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确"); //校验秒杀活动是否正在进行 }else if (itemModel.getPromoModel().getStatus().intValue() != 2) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动还未开始"); } } //落单减库存 boolean result = itemService.decreaseStock(itemId, amount); if (!result) { throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH); } //订单入库 OrderModel orderModel = new OrderModel(); orderModel.setUserId(userId); orderModel.setItemId(itemId); orderModel.setAmount(amount); if (promoId != null) { orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice()); }else { orderModel.setItemPrice(itemModel.getPrice()); } orderModel.setPromoId(promoId); orderModel.setOrderPrice(orderModel.getItemPrice().multiply(new BigDecimal(amount))); //生成交易流水号(订单号) orderModel.setId(this.generateOrderNo()); Order order = convertFromModel(orderModel); orderMapper.insertSelective(order); //增加商品销量 itemService.increaseSales(itemId,amount); //返回前端 return orderModel; } /** * @author Zhang Yifei * @date 2019/12/10 * @param orderModel * @return Order * @description 将orderModel转换为Order */ private Order convertFromModel(OrderModel orderModel) { if (orderModel == null) { return null; } Order order = new Order(); BeanUtils.copyProperties(orderModel, order); order.setItemPrice(orderModel.getItemPrice().doubleValue()); order.setOrderPrice(orderModel.getOrderPrice().doubleValue()); return order; } /** * @author Zhang Yifei * @date 2019/12/10 * @return String:订单号 * @description 生成订单号(如果事务createorder下单如果回滚,该下单方法中获得流水号id回滚,使等到的id号可能再一次被使用) */ @Transactional(propagation = Propagation.REQUIRES_NEW) String generateOrderNo() { //订单号16位 //前8位为时间信息 StringBuilder stringBuilder = new StringBuilder(); LocalDateTime now = LocalDateTime.now(); String nowDate = now.format(DateTimeFormatter.ISO_DATE).replace("-",""); stringBuilder.append(nowDate); //中间6位为自增序列 int seq = 0; Sequence sequence = sequenceMapper.getSequenceByName("order_info");//通過name拿到該行數據 seq = sequence.getCurrentValue(); sequence.setCurrentValue(sequence.getCurrentValue() + sequence.getStep()); sequenceMapper.updateByPrimaryKeySelective(sequence); //拼接(小于6位长度时后面补0!!!! String seqStr = String.valueOf(seq); for(int i = 0; i < 6-seqStr.length(); i++) { stringBuilder.append(0); } stringBuilder.append(seqStr); //最后2位为分库分表位,暂时写死 stringBuilder.append("00"); return stringBuilder.toString(); } }
7.orderController层 因为点击立即下单还是需要拿数据来产生一条订单数据,所以要有controller与前端交互来拿数据
/** * @author Zhang Yifei * @date 2019/12/7 10:39 * @description 处理前端对订单的请求 */ @Controller("order") @RequestMapping("/order") @CrossOrigin(allowCredentials = "true",allowedHeaders = "*") public class OrderController extends BaseController { @Autowired private OrderService orderService; @Autowired private HttpServletRequest httpServletRequest; /** * @author Zhang Yifei * @date 2019/12/10 * @param itemId 商品id * @param amount 数量 * @param promoId 秒杀id * @return CommonReturnType * @description 创建订单 封装下单请求 */ @RequestMapping(value = "/createorder", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED}) @ResponseBody public CommonReturnType createOrder(@RequestParam(name = "itemId") Integer itemId, @RequestParam(name = "amount") Integer amount, @RequestParam(name = "promoId", required = false) Integer promoId) throws BusinessException { Boolean isLogin = (Boolean) httpServletRequest.getSession().getAttribute("IS_LOGIN"); if (isLogin == null || !isLogin.booleanValue()) { throw new BusinessException(EmBusinessError.USER_NOT_LOGIN); } //获取用户信息 在域里面拿 可以去userCotroller核对一下 UserModel userModel = (UserModel)httpServletRequest.getSession().getAttribute("LOGIN_USER"); OrderModel orderModel = orderService.createOrder(userModel.getId(),itemId,promoId,amount); return CommonReturnType.create(null); } }
优化一下getitem商品详情页面(该页面下单),实现了库存减1销量加1,对应的数据库都有变化!!!
这个错误不小心把responsebody的参数名多打了一个
又错???这个前端页面不知道怎么搞了。
好了,打了一个半的远程加电话debug终于发现,是orderModelserviceimpl的第3步,订单入库那里错了两个造成空指针异常。
关于这次远程debug教学:
1.用step一步一步往下走不止看走不走得通,重要的是看变量数据是不是都进来都是预期的值,这个错误1错误2就是同学从ordermodel变量看到itemprice是null才发现的!!!!
2.return true点下一步进了非手写的类,但有可能并不是异常,进去后点绿色的箭头放过它,还能回到手写程序里,就好好看剩下的程序中的bug。。。
step overF8它还是可以进入调用的其他类的方法,但是要提前在其他类上也打好断点,顺利的话,会直接跳到调用的使用该方法其他类的断点处(对哪个都可以使该方法)。
如果是一个类里面的话,调用其他方法想进入,就用step inF7进到方法里,leetcode就这么debug的。。。
> Force Step Into (Alt + Shift + F7):强制步入,能进入任何方法,查看底层源码的时候可以用这个进入官方类库的方法。
> Step Out (Shift + F8):步出,从步入的方法内退出到方法调用处,此时方法已执行完毕,只是还没有完成赋值。(这个有用,就不用每次都关服务器了。但是并不是进入方法而多走了的好像不可用??)
> Drop Frame (默认无):回退断点,后面章节详细说明。
> Run to Cursor (Alt + F9):运行到光标处,你可以将光标定位到你需要查看的那一行,然后使用这个功能,代码会运行至光标行,而不需要打断点。
-----------------------------------------------------------------------------------------------
关于BigDecimal的model.price转成Double的DO.price: order.setItemPrice(orderModel.getItemPrice().doubleValue());
关于double转成bigdecimal:
//从数据库data向itemmodel转的时候也要注意price的类型问题
itemModel.setPrice(new BigDecimal(item.getPrice()));
-------------------------------------------------------------------------------------------------
至此交易环节完成。(好晕啊需要复习--------------------------------------------
开始下一环节,配置秒杀活动信息
第六章 秒杀模块开发
6.1 秒杀模型管理——活动模型创建
1.使用joda-time (替代用javaUtil的时间类,建立秒杀时间
<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.9</version> </dependency>
2.创建活动模型
之前我们已经有了用户模型,商品模型和订单模型,再创建一个秒杀模型为PromoModel (promo是营销的意思)
private DateTime startTime;//用的jodatime包的DateTime类
* @description 秒杀活动的领域模型 */ public class PromoModel { private Integer id; /** * 名称 */ private String promoName; /** * 活动状态:1表示未开始,2表示进行中,3表示已结束 */ private Integer status; /** * 开始时间 */ private DateTime startTime;//用的jodatime包的DateTime类 private DateTime endTime; /** * 适用商品 */ private Integer itemId; /** * 商品价格 */ private BigDecimal promoItemPrice; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getPromoName() { return promoName; } public void setPromoName(String promoName) { this.promoName = promoName; } public DateTime getStartTime() { return startTime; } public void setStartTime(DateTime startTime) { this.startTime = startTime; } public DateTime getEndTime() { return endTime; } public void setEndTime(DateTime endTime) { this.endTime = endTime; } public Integer getItemId() { return itemId; } public void setItemId(Integer itemId) { this.itemId = itemId; } public BigDecimal getPromoItemPrice() { return promoItemPrice; } public void setPromoItemPrice(BigDecimal promoItemPrice) { this.promoItemPrice = promoItemPrice; } public Integer getStatus() { return status; } public void setStatus(Integer status) { this.status = status; } }
3.设计数据库
CREATE TABLE `promo` ( `id` int(100) NOT NULL AUTO_INCREMENT, `promo_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL DEFAULT '', `start_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `end_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `item_id` int(11) NOT NULL DEFAULT 0, `promo_item_price` decimal(10, 2) NOT NULL DEFAULT 0.00, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Compact;
4.mybatis-generator自动产生数据库相关文件
<table tableName="promo" domainObjectName="PromoDO" enableCountByExample="false" enableUpdateByExample="false" enableDeleteByExample="false" enableSelectByExample="false" selectByExampleQueryId="false" ></table>
运行mvn
然后这里价格因为数据库设计时就用的decimal没有用double,所以生成的直接是 private BigDecimal promoItemPrice; 不用再考虑转化了。需要考虑的是时间的转化,DO起始时间是Date,Model里面是DateTime类。
转化时:promoModel.setStartTime(new DateTime(promoDO.getStartDate()));
6.2 秒杀模型管理——活动模型与商品模型结合
1.service
秒杀服务根据商品id,查询得到当前的活动以及其价格
PromoService
我们需要在商品详情页展示秒杀起止时间和秒杀价格,即展示promoModel
PromoModel getPromoByItemId(Integer itemId);
得在promoMapper添加promoDOMapper.selectByItemId(itemId)方法
<select id="selectByItemId" parameterType="java.lang.Integer" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from promo where item_id = #{itemId,jdbcType=INTEGER} </select>
PromoServiceImpl
写getPromoByItemId方法和do转model的方法
@Service public class PromoServiceImpl implements PromoService { @Autowired private PromoDOMapper promoDOMapper; //根据iremId获取即将开始的或者正在进行的活动 @Override public PromoModel getPromoByItemId(Integer itemId) { //获取商品对应的秒杀信息 PromoDO promoDO = promoDOMapper.selectByItemId(itemId); //dataobject->model PromoModel promoModel = convertFromDataObject(promoDO); if (promoModel == null) { return null; } //判断当前时间是否秒杀活动即将开始或正在进行 DateTime now = new DateTime(); if (promoModel.getStartDate().isAfterNow()) { promoModel.setStatus(1); } else if (promoModel.getEndDate().isBeforeNow()) { promoModel.setStatus(3); } else { promoModel.setStatus(2); } return promoModel; } private PromoModel convertFromDataObject(PromoDO promoDO) { if (promoDO == null) { return null; } PromoModel promoModel = new PromoModel(); BeanUtils.copyProperties(promoDO, promoModel); promoModel.setStartDate(new DateTime(promoDO.getStartDate())); promoModel.setEndDate(new DateTime(promoDO.getEndDate())); return promoModel; } }
关于判断当前活动状态,我们在PromoModel中添加一个Integer类型的属性status;
然后在impl中添加判断算法
//判断秒杀活动状态 1还未开始 2正在进行 3已经结束 if (promoModel.getStartTime().isAfterNow()) {//调用joda时间类的判断时间的方法 promoModel.setStatus(1); }else if (promoModel.getEndTime().isBeforeNow()) { promoModel.setStatus(3); }else { promoModel.setStatus(2); } return promoModel; }
然后我们要进入ItemService,回顾一下ItemModel的设计,当我们的item被赋予一个秒杀活动的时候,我们的itemModel需要聚合秒杀商品活动的属性,也就是说ItemModel里面需要引用PromoModel.
/** * 使用聚合模型,如果promoModel不为空,则表示其拥有还未结束的秒杀活动 */ private PromoModel promoModel;
对应的ItemServiceImpl的getItemById方法中拿到的ItemModel也需要增加商品秒杀活动的属性
* @description 通过id获取商品详情!前面创建商品返回时用 */ @Override public ItemModel getItemById(Integer id) { //获得商品实体类 Item item = itemMapper.selectByPrimaryKey(id); if (item == null) { return null; } //获得商品库存实体类 ItemStock itemStock = itemStockMapper.selectByItemId(item.getId()); //转化为ItemModel ItemModel itemModel = convertFromEntity(item, itemStock); //获取商品秒杀活动信息(这是第六章中加的部分) PromoModel promoModel = promoService.getPromoByItemId(itemModel.getId());//关联起来了 if (promoModel != null && promoModel.getStatus().intValue() != 3) {//排除活动不存在或活动已过期 itemModel.setPromoModel(promoModel); } return itemModel; }
同时也需要修改ItemVO商品cotroller层展示类,增加秒杀活动状态价格和ID等属性
/** * 记录商品是否在秒杀活动中,0:没有活动,1:待开始,2:正在进行 */ private Integer promoStatus;//秒杀活动状态 private BigDecimal promoPrice;//秒杀活动价格 private Integer promoId;//秒杀活动ID private String startTime;//秒杀活动开始时间 用来做倒计时展示用 这里不再用jodatime了,直接用string,因为要接送前端数据
然后去修改ItemController
修改由ItemModel转化为ItemVO的方法,因为要加上MODEL和VO新加的属性
* @description 将itemModel转换为ItemVO */ private ItemVO convertVOFromModel(ItemModel itemModel) { if (itemModel == null) { return null; } ItemVO itemVO = new ItemVO(); BeanUtils.copyProperties(itemModel, itemVO); if (itemModel.getPromoModel() != null) { //则有正在进行或即将开始的秒杀活动 下面是新加的属性 itemVO.setPromoStatus(itemModel.getPromoModel().getStatus()); itemVO.setPromoId(itemModel.getPromoModel().getId()); itemVO.setPromoPrice(itemModel.getPromoModel().getPromoItemPrice());
//VO里的时间是string类型的!所以得toSting一下 itemVO.setStartTime(itemModel.getPromoModel().getStartTime().toString(DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss"))); }else { itemVO.setPromoStatus(0);//没有活动 VO中的status设置为0 } return itemVO; }
3.然后对应修改商品展示页的前端getItem.html
加上秒杀价格,开始时间。
还有对应写判断状态显示秒杀时间,若秒杀活动未开始不让用户下单。前端开一个定时器做倒计时!!(不会写的话可以查一下
if (g_itemVO.promoStatus == 1) { // 秒杀活动还未开始 var startTime = g_itemVO.startTime.replace(new RegExp("-", "gm"), "/"); startTime = (new Date(startTime)).getTime(); var nowTime = Date.parse(new Date()); var delta = (startTime - nowTime) / 1000; if (delta <= 0) { // 活动开始了 g_itemVO.promoStatus = 2; reloadDom(); } $("#promoStartDate").text("秒杀活动将于:"+g_itemVO.startTime+" 开始售卖 倒计时:"+delta+" 秒"); $("#promoPrice").text(g_itemVO.promoPrice); $("#createOrder").attr("disabled", true); } else if (g_itemVO.promoStatus == 2) { // 秒杀活动进行中 $("#promoStartDate").text("秒杀活动进行中"); $("#promoPrice").text(g_itemVO.promoPrice); $("#createOrder").attr("disabled", false); $("#normalPriceContainer").hide(); }
整个的:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <!-- <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1"> --> <link href="static/assets/global/plugins/bootstrap/css/bootstrap.min.css" rel="stylesheet" type="text/css"/> <link href="static/assets/global/css/components.css" rel="stylesheet" type="text/css"/> <link href="static/assets/admin/pages/css/login.css" rel="stylesheet" type="text/css"/> <script src="static/assets/global/plugins/jquery-1.11.0.min.js" type="text/javascript"></script> </head> <body class="login"> <div class="content"> <h3 class="form-title">商品详情</h3> <div id="promoStartDateContainer" class="form-group"> <label style="color:blue" id="promoStatus" class="control-label"></label> <div> <label style="color:red" class="control-label" id="promoStartDate" /> </div> </div> <div class="form-group"> <div> <label class="control-label" id="title" /> </div> </div> <div class="form-group"> <div> <img style="width:200px;height:auto;" id="imgUrl"> </div> </div> <div class="form-group"> <label class="control-label">商品描述</label> <div> <label class="control-label" id="description" /> </div> </div> <div id="normalPriceContainer" class="form-group"> <label class="control-label">商品价格</label> <div> <label class="control-label" id="price" /> </div> </div> <div id="promoPriceContainer" class="form-group"> <label style="color:red" class="control-label">秒杀价格</label> <div> <label style="color:red" class="control-label" id="promoPrice" /> </div> </div> <div class="form-group"> <label class="control-label">商品库存</label> <div> <label class="control-label" id="stock" /> </div> </div> <div class="form-group"> <label class="control-label">商品销量</label> <div> <label class="control-label" id="sales" /> </div> </div> <div class="form-actions"> <button class="btn blue" id="createOrder" type="submit"> 立即购买 </button> </div> </div> </body> <script> var g_itemVO = {}; $(document).ready(function() { // 获取商品详情 $.ajax({ type: "GET", url: "http://localhost:8081/item/get", data: { "id": getParam("id"),//下面写的script方法 }, xhrFields:{ withCredentials:true }, success: function(data) { if (data.status == "success") { g_itemVO = data.data; reloadDom(); setInterval(reloadDom, 1000); } else { alert("获取信息失败,原因为" + data.data.errMsg); } }, error: function(data) { alert("获取信息失败,原因为" + data.responseText); } }); $("#createOrder").on("click", function() { $.ajax({ type: "POST", url: "http://localhost:8081/order/createorder", contentType: "application/x-www-form-urlencoded", data: { "itemId": g_itemVO.id, "promoId": g_itemVO.promoId, "amount": 1, }, xhrFields:{ withCredentials:true }, success: function(data) { if (data.status == "success") { alert("下单成功"); window.location.reload(); } else { alert("下单失败,原因为" + data.data.errMsg + data.data.errCode); if (data.data.errCode == 20003) { window.location.href="login.html"; } } }, error: function(data) { alert("下单失败,原因为" + data.responseText); } }); }); }); function reloadDom() { $("#title").text(g_itemVO.title); $("#imgUrl").attr("src", g_itemVO.imgUrl); $("#description").text(g_itemVO.description); $("#price").text(g_itemVO.price); $("#stock").text(g_itemVO.stock); $("#sales").text(g_itemVO.sales); if (g_itemVO.promoStatus == 1) { // 秒杀活动还未开始 var startTime = g_itemVO.startTime.replace(new RegExp("-", "gm"), "/"); startTime = (new Date(startTime)).getTime(); var nowTime = Date.parse(new Date()); var delta = (startTime - nowTime) / 1000; if (delta <= 0) { // 活动开始了 g_itemVO.promoStatus = 2; reloadDom(); } $("#promoStartDate").text("秒杀活动将于:"+g_itemVO.startTime+" 开始售卖 倒计时:"+delta+" 秒"); $("#promoPrice").text(g_itemVO.promoPrice); $("#createOrder").attr("disabled", true); } else if (g_itemVO.promoStatus == 2) { // 秒杀活动进行中 $("#promoStartDate").text("秒杀活动进行中"); $("#promoPrice").text(g_itemVO.promoPrice); $("#createOrder").attr("disabled", false); $("#normalPriceContainer").hide(); } } function getParam(paramName) { paramValue = "", isFound = !1; if (this.location.search.indexOf("?") == 0 && this.location.search.indexOf("=") > 1) { arrSource = unescape(this.location.search).substring(1, this.location.search.length).split("&"), i = 0; while (i < arrSource.length && !isFound) arrSource[i].indexOf("=") > 0 && arrSource[i].split("=")[0].toLowerCase() == paramName.toLowerCase() && (paramValue = arrSource[i].split("=")[1], isFound = !0), i++ } return paramValue == "" && (paramValue = null), paramValue } </script> </html>
接下来对下单逻辑进行修正,使没到活动时不让下单,订单上的价格要增加秒杀活动下单的价格
4.修改OrderModel
增加秒杀相关属性(跟之前修改itemModel流程差不多
//若非空,则表示是以秒杀商品方式下单 private Integer promoId; //购买时商品的单价,若promoId非空,则表示是以秒杀商品方式下单 private BigDecimal itemPrice;
然后在数据库中,DO中,DOMapper中增加此字段(手动。。。
5.改造下单接口OderService
//1.通过url上传过来秒杀活动id,然后下单接口内校验对应id是否属于对应商品且活动已开始 //2.直接在下单接口内判断对应的商品是否存在秒杀活动,若存在进行中的则以秒杀价格下单 //倾向于使用第一种形式,因为对同一个商品可能存在不同的秒杀活动,而且第二种方案普通销售的商品也需要校验秒杀 OrderModel createOrder(Integer userId, Integer itemId, Integer promoId, Integer amount) throws BusinessException;
OderServiceImpl中修改继承的方法
@Override @Transactional public OrderModel createOrder(Integer userId, Integer itemId,Integer promoId, Integer amount) throws BusinessException { //校验下单状态,商品是否存在,用户是否合法,数量是否正确,活动信息 ItemModel itemModel = itemService.getItemById(itemId); if (itemModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "商品信息不存在"); } UserModel userModel = userService.getUserById(userId); if (userModel == null) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "用户信息不存在"); } if (amount <= 0 || amount > 99) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "购买数量不正确"); } if (promoId != null) { //校验活动是否存在适用商品 if (promoId.intValue() != itemModel.getPromoModel().getId()) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动信息不正确"); //校验秒杀活动是否正在进行 }else if (itemModel.getPromoModel().getStatus().intValue() != 2) { throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "活动还未开始"); } } //落单减库存 boolean result = itemService.decreaseStock(itemId, amount); if (!result) { throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH); } //订单入库 OrderModel orderModel = new OrderModel(); orderModel.setUserId(userId); orderModel.setItemId(itemId); orderModel.setAmount(amount); if (promoId != null) { orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());//取秒杀价格 }else { orderModel.setItemPrice(itemModel.getPrice());//取平销价格 } orderModel.setPromoId(promoId); orderModel.setOrderPrice(orderModel.getItemPrice().multiply(new BigDecimal(amount)));//单价乘以数量 //生成交易流水号(订单号) orderModel.setId(this.generateOrderNo()); Order order = convertFromModel(orderModel); orderMapper.insertSelective(order); //增加商品销量 itemService.increaseSales(itemId,amount); //返回前端 return orderModel; }
6.然后改controller层
订单不需要VO所以不必修改。OderController的creatOrder方法要增加接收前端传来的请求参数:promoId.如果没有收到则是平销价格
@RequestParam(name = "promoId",required = false) Integer promoId,
OrderModel orderModel = orderService.createOrder(userModel.getId(),itemId,promoId,amount);
7.继续修改前端getItem界面 下单时候需要增加promoId这个参数给服务器
测试
下单后库存和销量对应减少和增加了1位
查看数据库的订单情况:增加了一条订单
至此我们完成了电商秒杀项目的用户注册登录,普通商品下单,秒杀商品下单 三大块。
第七章 课程总结
1.学会了使用Springboot+Mybatis完成javaweb项目的搭建,学会一个电商秒杀系统的基本流程及代码实现
2.该项目的框架
首先使用了前后端分离的设计方式。
前端使用了html,css,jquery和图片,以及metronic对应的前端框架来完成用户注册登录以及商品展示下单交易秒杀倒计时等基本前端功能。
然后接入层使用了springMVC的controller定义了view object模型返回了通用对象,并且结合通用对象进行了通用异常处理,用前后端分离返回了通用的jsondata模型。
之后在业务层使用了对应的mybatis的接入,以及Model领域模型的概念,完成了对应的用户服务,商品服务,交易服务,活动服务等业务层。
数据层使用@transition完成事务的切面,数据Dao完成对数据库的相关操作。之后的实战课会引入本地缓存和集中式缓存,解决性能相关问题。
资源:metronic框架里面可以根据template练习使用
遗留问题:(实战课。。。。
如何支撑亿级流量