目标
- 掌握跨域请求CORS解决方案
- 完成结算页收货人地址选择功能
- 完成结算页支付方式选择
- 完成结算页商品清单功能
- 完成保存订单功能
1. 商品详情页跨域请求
1. 需求分析
从商品详情页点击“加入购物车”按钮,将当前商品加入购物车,并跳转到购物车页面
2. JS跨域请求
1. 什么是域
当两个应用协议、主机地址(或域名)、端口其中有一项不同,就认为他们的域不同。
这里的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面的不同域的框架中(iframe)的数据。
3. 跨域调用测试
从page-web的itemController.js,引入$http服务,修改addToCart()方法
//添加商品到购物车
$scope.addToCart=function(){
// alert('SKUID:'+$scope.sku.id );
$http.get("http://localhost:9107/cart/addGoodsToCartList.do?itemId="
+$scope.sku.id+"&num="+$scope.num).success(
function (response) {
if(response.success){
location.href="http://localhost:9107/cart";
}else{
alert(response.message);
}
}
);
}
发现无法跨域,
XMLHttpRequest cannot load
4. 跨域解决方案CORS
CORS是一个w3c标准,全称为“跨域资源共享”(Cross-origin response sharing)。CORS需要浏览器和服务器同时支持。目前所有浏览器都支持,IE不能低于IE10
允许浏览器向跨源服
务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。整个CORS通信过程,都是浏览器自动完成,不需要用户参与。实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
请求过程如下:
Preflight Request:
然后服务器端给我们返回一个Preflight Response
下面开始让购物车工程接收跨域请求
- 修改cart-web的CartController.java的addGoodsToCartList方法。添加代码
response.setHeader("Access-Control-Allow-Origin", "http://localhost:9105");
response.setHeader("Access-Control-Allow-Credentials", "true");
Access-Control-Allow-Origin是HTML5定义的一种解决资源跨域的策略
用过服务器端返回带有Access-Control-Allow-Origin表示的ResponseHeader,解决资源的跨域权限问题。
在response添加Access-Control-Allow-Origin,也可以设置为
*
表示该资源谁都可以用
- 修改item-web的itemController.js
//添加商品到购物车
$scope.addToCart=function(){
// alert('SKUID:'+$scope.sku.id );
$http.get("http://localhost:9107/cart/addGoodsToCartList.do?itemId="
+$scope.sku.id+"&num="+$scope.num,{"withCredentials":true}).success(
function (response) {
if(response.success){
location.href="http://localhost:9107/cart";
}else{
alert(response.message);
}
}
);
}
调用测试,可以实现跨域。
CORS请求默认不发送cookie和HTTP认证信息。如果要把cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials,另一方面,需要在ajax请求中打开withCredentials属性。
5. SpringMVC跨域注解
4.2及以上版本可以使用注解跨域。需要跨域的方法上添加注解
@CrossOrigin
后面的allowCredentials="true"可以省略
2. 结算页- 收件人地址选择
1. 需求和数据库分析
1. 需求描述
在结算页实现收件人地址选择功能
2. 数据库表结构分析
Tb_address为地址表,user_id其实就是user表的username
2. 准备工作
1. 生成代码
- AddressService接口拷入user-interface
- AddressServiceImpl拷入user-service
- cart-web引入user-interface依赖,AddressController拷入cart-web
2. 拷贝页面资源
getOrderInfo.html拷贝到cart-web的webapp下
3. 实现地址列表
1. 后端代码
- 修改user-interface的AddressService.java
- 修改user-service的AddressServiceImpl.java
@Override
public List<TbAddress> findListByUserId(String userId) {
TbAddressExample example = new TbAddressExample();
Criteria criteria = example.createCriteria();
criteria.andUserIdEqualTo(userId);
return addressMapper.selectByExample(example);
}
- 修改cart-web的AddressController.java
@RequestMapping("/findListByLoginUser")
public List<TbAddress> findListByLoginUser(){
// 获取登录用户
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return addressService.findListByUserId(username);
}
2. 前端代码
- cart-web的cartService.js
// 获取地址列表
this.findAddressList = function () {
return $http.get('address/findListByLoginUser.do');
}
- cart-web的cartController.js
// 获取当前用户的地址列表
$scope.findAddressList = function () {
cartService.findAddressList().success(
function (response) {
$scope.addressList = response;
}
);
}
- 修改getOrderInfo.html
<ul class="addr-detail">
<li class="addr-item">
<div ng-repeat="address in addressList">
<div class="con name "><a href="javascript:;" >{{address.contact}}<span title="点击取消选择"> </a></div>
<div class="con address">{{address.address}} <span>{{address.mobile}}</span>
<span class="base" ng-if="address.isDefault=='1'">默认地址</span>
<span class="edittext"><a data-toggle="modal" data-target=".edit" data-keyboard="false" >编辑</a> <a href="javascript:;">删除</a></span>
</div>
<div class="clearfix"></div>
</div>
</li>
</ul>
<script type="text/javascript" src="plugins/angularjs/angular.min.js"> </script>
<script type="text/javascript" src="js/base.js"> </script>
<script type="text/javascript" src="js/service/cartService.js"> </script>
<script type="text/javascript" src="js/controller/cartController.js"> </script>
4. 地址的选择
- 在orderInfoController.js增加代码
// 选择地址
$scope.selectAddress = function (address) {
$scope.address = address;
}
// 判断某地址对象是不是当前选择的地址
$scope.idSelectedAddress = function (address) {
if(address===$scope.address){
return true;
}else{
return false;
}
}
- 修改页面-点击选择
- 修改页面,显示选择的地址
5. 默认地址显示
修改orderInfoController.js
// 获取当前用户的地址列表
$scope.findAddressList = function () {
cartService.findAddressList().success(
function (response) {
$scope.addressList = response;
for(var i=0;i<$scope.addressList.length;i++){
if($scope.addressList[i].isDefault==='1'){
$scope.address = $scope.addressList[i];
break;
}
}
}
);
}
6. 收件人地址增删改
TODO
3. 结算页-支付方式选择
1. 需求分析
实现支付方式的选择,品优购支持两种支付方式:微信支付和货到付款
2. 支付方式的选择
1. 前端控制层
cartController.js
$scope.order = {paymentType:'1'};//订单对象
// 选择支付方式
$scope.selectPayType = function (type) {
$scope.order.paymentType = type;
}
2. 页面
getOrderInfo.html
<li class="selected" ng-click="selectPayType('1')">微信付款<span title="点击取消选择"></span></li>
<li ng-click="selectPayType('2')">货到付款<span title="点击取消选择"></span></li>
4. 结算页-商品清单与金额显示
1. 需求分析
显示购物车的商品清单和合计数量、金额
2. 显示商品清单
- 页面getOrderInfo.html上初始化调用
- 循环显示商品清单
<ul class="send-detail" ng-repeat="orderItem in cart.orderItemList">
<li>
<div class="sendGoods">
<ul class="yui3-g">
<li class="yui3-u-1-6">
<span><img width="100px" height="100px" src="{{orderItem.picPath}}"/></span>
</li>
<li class="yui3-u-7-12">
<div class="desc">{{orderItem.title}}</div>
<div class="seven">7天无理由退货</div>
</li>
<li class="yui3-u-1-12">
<div class="price">¥{{orderItem.price.toFixed(2)}}</div>
</li>
<li class="yui3-u-1-12">
<div class="num">X{{orderItem.num}}</div>
</li>
<li class="yui3-u-1-12">
<div class="exit">有货</div>
</li>
</ul>
</div>
</li>
</ul>
3. 显示合计金额
修改getOrderInfo.html
<div class="order-summary">
<div class="static fr">
<div class="list">
<span><i class="number">{{totalValue.totalNum}}</i>件商品,总商品金额</span>
<em class="allprice">¥{{totalValue.totalMoney.toFixed(2)}}</em>
</div>
<div class="list">
<span>返现:</span>
<em class="money">0.00</em>
</div>
<div class="list">
<span>运费:</span>
<em class="transport">0.00</em>
</div>
</div>
</div>
<div class="clearfix trade">
<div class="fc-price">应付金额: <span class="price">¥{{totalValue.totalMoney.toFixed(2)}}</span></div>
5. 保存订单
1. 需求分析
1. 需求描述
点击订单结算页的提交订单,将购物车保存到订单表和订单明细表,并将购物车数据清除。
2. 数据库表结构分析
Tb_order为订单主表
2. 准备工作
1. 搭建框架
- 创建order-interface 引入依赖pojo
- 创建order-service,参见content-service,添加各种配置,dubbox端口20888,tomcat端口9008
- cart-web引入依赖order-interface
2. 生成代码
根据之前生成的user相关的代码,拷贝到工程order-interface、order-service和cart-web
3. 分布式ID生成器
为什么不让order表的主键自增?
对于互联网应用,可能某个表会占用很大的存储空间,比如订单表,每天都会生成很多订单,如果服务器的硬盘满了怎么办?
采用数据库分片,把一个数据库进行拆分,通过数据库中间件进行连接
如果采用数据自增,可能会产生重复的ID
分布式ID生成解决方案:
- UUID(不用的理由:1. 太长;2. 没办法排序)
- Redis(产生自增的序号 主键的生成需要访问redis,对redis有依赖)
- Oracle 数据库对象-序列(与表无关) 只有数据库用Oracle才行
- 程序自己写算法(不重复)
为什么无需的UUID导致入库性能变差呢?
涉及到B+树索引的分类,关系型数据库的索引大多是B+树。
采用开源的Twitter的snowflake算法
- 第一位:占用1bit,值为0,没有实际作用
- 时间戳:占用41bit,精确到毫秒,总共可容纳约69年时间
- 工作机器id:占用10bit,高位5bit是数据中心id,低位5bit为工作节点id,最多容纳1024个节点
- 序列号:占用12bit,该值在同一毫秒同一节点从0开始不断累加,最多累加到4095。
雪花算法在同一毫秒最多可生成1024*4096=42万左右个id
3. 后端代码
1. 服务实现层
order-service的OrderServiceImpl.java
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private IdWorker idWorker;
@Autowired
private TbOrderItemMapper orderItemMapper;
@Override
public void add(TbOrder order) {
// 1. 从redis中提取购物车列表
List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(order.getUserId());
// 2. 循环购物车列表添加订单
for (Cart cart : cartList) {
TbOrder tbOrder = new TbOrder();
long orderId = idWorker.nextId();
tbOrder.setOrderId(orderId);
tbOrder.setPaymentType(order.getPaymentType());// 付款方式
tbOrder.setStatus("1");//未付款
tbOrder.setCreateTime(new Date());
tbOrder.setUpdateTime(new Date());
tbOrder.setUserId(order.getUserId());
tbOrder.setReceiverAreaName(order.getReceiverAreaName());//收货人地址
tbOrder.setReceiverMobile(order.getReceiverMobile());//收货人电话
tbOrder.setReceiver(order.getReceiver());// 收货人
tbOrder.setSourceType(order.getSourceType());//订单来源
tbOrder.setSellerId(order.getSellerId());//商家id
double money = 0;//合计数
// 循环购物车中每条明细记录
for (TbOrderItem orderItem : cart.getOrderItemList()) {
orderItem.setId(idWorker.nextId());
orderItem.setOrderId(orderId);//订单编号
// 注意:商家id来自购物车
orderItem.setSellerId(cart.getSellerId());//商家id
orderItemMapper.insert(orderItem);
money += orderItem.getTotalFee().doubleValue();
}
tbOrder.setPayment(new BigDecimal(money));//合计
orderMapper.insert(tbOrder);
}
// 3. 清除redis中的购物车
redisTemplate.boundHashOps("cartList").delete(order.getUserId());
}
记得在类上添加事务注解
在spring的配置applicationContext-service中添加idWorker
<bean id="idWorker" class="util.IdWorker">
<!--进程id-->
<constructor-arg index="0" value="0"/>
<!--数据中心id-->
<constructor-arg index="1" value="0"/>
</bean>
3. 控制层
修改cart-web的OrderController.java
修改add方法
@RequestMapping("/add")
public Result add(@RequestBody TbOrder order){
// 获取当前登录人账号
String username = SecurityContextHolder.getContext().getAuthentication().getName();
order.setUserId(username);
order.setSourceType("2");//订单来源 PC
try {
orderService.add(order);
return new Result(true, "增加成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "增加失败");
}
}
4. 前端代码
1. 服务层
修改cart-web的cartService.js
// 提交订单
this,submitOrder = function (order) {
return $http.post('order/add.do',order);
}
2. 控制层
修改cartController.js
// 保存订单
$scope.submitOrder = function () {
$scope.order.receiverAreaName=$scope.address.address;
$scope.order.receiverMobile = $scope.address.mobile;
$scope.order.receiver = $scope.address.contact;//联系人
cartService.submitOrder($scope.order).success(
function (response) {
alert(response.message);
}
);
}
3. 测试
最终需要跑cart-web,依赖user-service cart-service order-service
中间小bug,开始时不出现收货地址和收货手机号,发现controller写错了,receiverAreaName和receiverMobile
后来发现数据库没有存sellerid,发现sellerid应该是来自cart,写成了order
4. 完善
// 保存订单
$scope.submitOrder = function () {
$scope.order.receiverAreaName=$scope.address.address;
$scope.order.receiverMobile = $scope.address.mobile;
$scope.order.receiver = $scope.address.contact;//联系人
cartService.submitOrder($scope.order).success(
function (response) {
if(response.success){
if($scope.order.paymentType==='1'){//微信支付,跳转到支付页面
location.href="pay.html";
}else{
location.href="paysuccess.html";
}
}else{
alert(response.message);//也可以跳转提示页面
}
}
);
}