2022-05-28  2022-05-28    6647 字   14 分钟

第十三章

提交订单

优就业.JAVA教研室

学习目标

  • 登录页的配置
  • 登录成功跳转实现
  • 结算页查询实现
  • 下单实现
  • 变更库存
  • 增加积分

1 登录页面配置

前面使用的都是采用Postman实现登录,接着我们实现一次oauth自定义登录。

1.1 准备工作

(1)静态资源导入

资料/页面/前端登录相关的静态资源导入到dongyimai-user-oauth中,如下图。

注意:为了测试在static目录编写一个测试页success.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录成功</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>

(2)引入thymeleaf

修改dongyimai-user-oauth,引入thymeleaf模板引擎

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

(3)登录配置

修改dongyimai-user-oauth,编写一个控制器com.offcn.oauth.controller.LoginRedirect,实现登录页跳转,代码如下:

@Controller
@RequestMapping(value = "/oauth")
public class LoginRedirect {

    /***
     * 跳转到登录页面
     * @return
     */
    @GetMapping(value = "/login")
    public String login(){
        return "login";
    }
}

(4)登录页配置

针对静态资源和登录页面,我们需要实现忽略安全配置,并且要指定登录页面。修改com.offcn.oauth.config.WebSecurityConfig的2个configure方法,代码如下:

第1个configure配置:

第2个configure配置:

测试

http://localhost:9100/oauth/login

1.2 登录实现

点击登录按钮,访问之前的登录方法实现登录,我们需要对登录页做一下调整。

(1)引入thymeleaf命名空间

修改login.html,引入命名空间

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

(2)登录脚本

点击登录按钮,使用vue+axios实现登录,我们需要定义脚本访问后台登录方法。

先添加vue入口标签:修改login.html,在73行左右的标签上添加id=“app”,代码如下:

<div class="login-box" id="app">
    <!--head-->
		<div class="py-container logoArea">
			<a href="" class="logo"></a>
		</div>
    
    		…………略
    
</div>

引入js

<script src="/js/vue.js" th:src="@{/js/vue.js}"></script>
<script src="/js/axios.js" th:src="@{/js/axios.js}"></script>

登录脚本实现:

<script th:inline="javascript">
	var app = new Vue({
		el:"#app",
		data:{
			username:'',
			password:''
		},
		methods:{
			login:function () {
				axios.post('/user/login?username='+this.username+'&password='+this.password).then(
				    function (response) {
						if(response.data.flag){//登录成功
						   alert("登录成功");
						 location.href="/success.html";
						}else{
						    alert(response.data.message);
						}
                    }
				)
            }
		}
	})
</script>

(3)表单修改

<div class="sui-form">
                            <div class="input-prepend"><span class="add-on loginname"></span>
                                <input id="prependedInput" v-model="username" type="text" placeholder="邮箱/用户名/手机号"
                                       class="span2 input-xfat">
                            </div>
                            <div class="input-prepend"><span class="add-on loginpwd"></span>
                                <input id="prependedInput" v-model="password" type="password" placeholder="请输入密码"
                                       class="span2 input-xfat">
                            </div>
                            <div class="setting">
                                <label class="checkbox inline">
                                    <input name="m1" type="checkbox" value="2" checked="">
                                    自动登录
                                </label>
                                <span class="forget">忘记密码?</span>
                            </div>
                            <div class="logined">
                                <button class="sui-btn btn-block btn-xlarge btn-danger" @click="login()">
&nbsp;&nbsp;                                </button>
                            </div>
</div>

注意表单:<form>改成<div>避免出现点击登录按钮默认提交表单的情况。

(4)测试

输入账号:dongyimai 密码: 123

登录成功,跳转到测试主页:

1.3 登录跳转

用户没有登录的时候,我们直接访问购物车,效果如下:

http://localhost:8001/api/cart/findCartList

我们可以发现,返回的只是个错误状态码,不方便测试,我们可以重定向到登录页面,让用户登录,我们可以修改网关的头文件,让用户每次未登录的时候,都跳转到登录页面。

修改dongyimai-gateway-web的com.offcn.filter.AuthorizeFilter,代码如下:

修改判断token为空的代码,把原来响应405,修改成跳转到登录网关登录页地址

单独编写跳转方法:needAuthorization

 //设置跳转方法
    private Mono<Void> needAuthorization(String url, ServerWebExchange exchange){
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.SEE_OTHER);
        response.getHeaders().set("Location",url);
        return response.setComplete();
    }

此时再测试,就可以跳转到登录页面了。当然,在工作中,这里不能直接跳转到登录页,应该提示状态给页面,让页面根据判断跳转,这里只是为了方便测试。

1.4 成功登录跳转到原访问页

上面虽然实现了登录跳转,但登录成功后却并没有返回到要访问的购物车页面,我们可以将用户要访问的页面作为参数传递给登录控制器,登录控制器记录下来,每次登录成功后,再跳转记录访问路劲参数指定的页面即可。

(1)修改网关携带当前URI

修改dongyimai-gateway-web的com.offcn.filter.AuthorizeFilter,在之前的URL后面添加FROM参数以及FROM参数的值为request.getURI(),代码如下:

注意:跳转地址,需要做UrlEncode编码处理

(2)认证服务器获取FROM参数

修改dongyimai-user-oauth的com.offcn.oauth.controller.LoginRedirect记录访问来源页,代码如下:

注意:接收过来的跳转url需要做Urldecode解码

代码如下:

@Controller
@RequestMapping(value = "/oauth")
public class LoginRedirect {

    /***
     * 跳转到登录页面
     * @return
     */
    @GetMapping(value = "/login")
    public String login(@RequestParam(value = "FROM",required = false,defaultValue = "") String from, Model model){
        try {
            String decodeUrl = URLDecoder.decode(from, "utf-8");

            model.addAttribute("from",decodeUrl);
            System.out.println("form:"+decodeUrl);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return "login";
    }
}

修改页面,获取来源页信息,并存到from变量中,登录成功后跳转到该地址。

<script th:inline="javascript">
    var app = new Vue({
        el: "#app",
        data: {
            username: '',
            password: ''           
        },
        methods: {
            login: function () {
                axios.post('/user/login?username=' + this.username + '&password=' + this.password).then(
                    function (response) {
                        if (response.data.flag) {//登录成功
                           
                            location.href = [[$from]];//需要跳转到的地址
                        } else {
                            response.data.message;
                        }
                    }
                )
            }
        }
    })
</script>

此时再测试,就可以识别未登录用户,跳转到登录页,然后根据登录状态,如果登录成功,则跳转到来源页。

2 订单确认页

2.1 收件地址分析

用户从购物车页面点击结算,跳转到订单结算页,结算页需要加载用户对应的收件地址,如下图:

表结构分析:

我们可以根据用户登录名去tb_address表中查询对应的数据。

2.2 实现用户收件地址查询

2.2.1 代码实现

(1)业务层

业务层接口

修改dongyimai-user-service微服务,需改com.offcn.user.service.AddressService接口,添加根据用户名字查询用户收件地址信息,代码如下:

    /**
     * 根据用户查询地址
     * @param userId
     * @return
     */
    public List<Address> findListByUserId(String userId );

业务层接口实现类

修改dongyimai-service-user微服务,修改com.offcn.user.service.impl.AddressServiceImpl类,添加根据用户查询用户收件地址信息实现方法,如下代码:

/**
 * 根据用户查询地址
 *
 * @param userId
 * @return
 */
@Override
public List<Address> findListByUserId(String userId) {
    QueryWrapper<Address> queryWrapper = new QueryWrapper<>();
    queryWrapper.eq("user_id",userId);
    //根据构建的条件查询数据
    return this.list(queryWrapper);
}

(2)控制层

修改dongyimai-user-service微服务,修改com.offcn.user.controller.AddressController,添加根据用户名查询用户收件信息方法,代码如下:

@Autowired
    private TokenDecode tokenDecode;

    /****
     * 用户收件地址
     */
    @GetMapping(value = "/user/list")
    public Result<List<Address>> findListByUserId(){
        //获取用户登录信息
        Map<String, String> userMap = tokenDecode.getUserInfo();
        String userId = userMap.get("user_name");
        //查询用户收件地址
        List<Address> addressList = addressService.findListByUserId(userId);
        return new Result(true, StatusCode.OK,"查询成功!",addressList);
    }

注意要在主启动类增加TokenDecode的声明

public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class,args);
    }
    @Bean
    public TokenDecode getTokenDecode(){
        return new TokenDecode();
    }
}

2.2.2 测试

访问 http://localhost:8001/api/address/user/list

2.2.3 运送清单【学员完成】

运送清单其实就是购物车列表,直接查询之前的购物车列表即可,这里不做说明了。

3 下单

3.1 业务分析

点击结算页的时候,会立即创建订单数据,创建订单数据会将数据存入到2张表中,分别是订单表和订单明细表,此处还需要修改商品对应的库存数量。

订单表结构如下:

订单明细表结构如下:

3.2 下单实现

下单的时候,先添加订单往tb_order表中增加数据,再添加订单明细,往tb_order_item表中增加数据。

3.2.1分布式ID生成器

我们采用的是开源的twitter( 非官方中文惯称:推特.是国外的一个网站,是一个社交网络及微博客服务) 的snowflake算法。

这里先修改dongyimai-order-service微服务,实现下单操作,这里会生成订单号,我们首先需要在启动类中创建一个IdWorker对象。

com.offcn.OrderApplication中创建IdWorker,代码如下:

@Bean
public IdWorker idWorker(){
    return new IdWorker(1,1);
}

修改Pojo下的Order、OrderItem Id生成方式:

(1)业务层

修改dongyimai-order-service微服务,修改com.offcn.order.service.impl.OrderServiceImpl,代码如下:

  //注入redis操作类
    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Autowired
    private IdWorker idWorker;

/**
     * 增加Order
     * @param order
     */
    @Override
    public void add(Order order){
        // 得到购物车数据
        List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(order.getUserId());
        for (Cart cart : cartList) {
            long orderId = idWorker.nextId();
            System.out.println("sellerId:" + cart.getSellerId());
            Order tborder = new Order();// 新创建订单对象
            //复制前端传递过来的订单的属性值,到新创建订单对象
            BeanUtils.copyProperties(order,tborder);
            tborder.setOrderId(orderId);// 订单ID(一定要在复制属性后设置)
            tborder.setStatus("1");// 状态:未付款
            tborder.setCreateTime(new Date());// 订单创建日期
            tborder.setUpdateTime(new Date());// 订单更新日期
            tborder.setSellerId(cart.getSellerId());// 商家ID
            // 循环购物车明细
            double money = 0;
            for (OrderItem orderItem : cart.getOrderItemList()) {
                orderItem.setId(idWorker.nextId());
                orderItem.setOrderId(orderId);// 订单ID
                orderItem.setSellerId(cart.getSellerId());
                money += orderItem.getTotalFee().doubleValue();// 金额累加
                //保存购物明细
                orderItemMapper.insert(orderItem);
            }
            tborder.setPayment(new BigDecimal(money));
            //保存订单
            this.save(tborder);
        }
        //下单成功,清空redis购物车数据
        redisTemplate.boundHashOps("cartList").delete(order.getUserId());


    }

(2)控制层

修改dongyimai-order-service微服务,修改com.offcn.order.controller.OrderController类,代码如下:

@Autowired
private TokenDecode tokenDecode;

/***
 * 新增Order数据
 * @param order
 * @return
 */
@ApiOperation(value = "Order添加",notes = "添加Order方法详情",tags = {"OrderController"})
@PostMapping
public Result add(@RequestBody  @ApiParam(name = "Order对象",value = "传入JSON数据",required = true)  Order order){
    //获取用户名
    Map<String, String> userMap = tokenDecode.getUserInfo();
    String username = userMap.get("user_name");
    //设置购买用户
    order.setUserId(username);
    orderService.add(order);
    return new Result(true,StatusCode.OK,"添加成功");
}

3.2.2 测试

使用postMan

保存订单测试,表数据变化如下:

tb_order表数据:

tb_order_item表数据:

3.3 库存变更

3.3.1 业务分析

上面操作只实现了下单操作,但对应的库存还没跟着一起减少,我们在下单之后,应该调用商品微服务,将下单的商品库存减少,销量增加。每次订单微服务只需要将用户名传到商品微服务,商品微服务通过用户名到Redis中查询对应的购物车数据,然后执行库存减少,库存减少需要控制当前商品库存>=销售数量。

如何控制库存数量>=销售数量呢?其实可以通过SQL语句实现,每次减少数量的时候,加个条件判断。

where num>=#{num}即可。

3.3.2 代码实现

要调用其他微服务,需要将头文件中的令牌数据携带到其他微服务中取,所以我们不能使用hystrix的多线程模式,修改dongyimai-sellergoods–service的applicatin.yml配置,代码如下:

#hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE    #使用Seamphore,你创建了多少线程,实际就会有多少线程进行执行,只是可同时执行的线程数量会受到限制

每次还需要使用拦截器添加头文件信息

修改配置类com.offcn.SellergoodsApplication添加Feign请求拦截器,代码如下:

    @Bean
    public FeignInterceptor feignInterceptor(){
        return new FeignInterceptor();
    }

(1)Dao层

修改dongyimai-sellergoods-service微服务的com.offcn.sellergoods.dao.ItemMapper接口,增加库存递减方法,代码如下:

   /**
     * 递减库存
     * @param orderItem
     * @return
     */
    @Update("UPDATE tb_item SET num=num-#{num} WHERE id=#{itemId} AND num>=#{num}")
    int decrCount(OrderItem orderItem);

(2)业务层

修改dongyimai-sellergoods-service微服务的com.offcn.sellergoods.service.ItemService接口,添加如下方法:

/***
 * 库存递减
 * @param username
 */
void decrCount(String username);

修改dongyimai-sellergoods-service微服务的com.offcn.sellergoods.service.impl.ItemServiceImpl实现类,添加一个实现方法,代码如下:

    @Autowired
    private ItemMapper itemMapper;

    @Autowired
    private RedisTemplate redisTemplate;

/***
 * 库存递减
 * @param username
 */
@Override
public void decrCount(String username) {
   // 得到购物车数据
        List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(username);
        //遍历购物车集合
        for (Cart cart : cartList) {
            //遍历购物明细
            for (OrderItem orderItem : cart.getOrderItemList()) {
                //递减库存
                int count = itemMapper.decrCount(orderItem);
                if(count<=0){
                    throw new RuntimeException("库存不足,递减失败!");
                }
            }
        }
}

(3)控制层

修改dongyimai-sellergoods-service的com.offcn.sellergoods.controller.ItemController类,添加库存递减方法,代码如下:

/***
 * 库存递减
 * @param username
 * @return
 */
@PostMapping(value = "/decr/count")
public Result decrCount(String username){
    //库存递减
    itemService.decrCount(username);
    return new Result(true,StatusCode.OK,"库存递减成功!");
}

(4)创建feign

同时在dongyimai-sellergoods-service-api工程添加com.offcn.sellergoods.feign.ItemFeign的实现,代码如下:

/***
 * 库存递减
 * @param username
 * @return
 */
@PostMapping(value = "/decr/count")
Result decrCount(@RequestParam(value = "username") String username);

3.3.3 调用库存递减

修改dongyimai-order-service微服务的com.offcn.order.service.impl.OrderServiceImpl类的add方法,增加库存递减的调用。

先注入ItemFeign

@Autowired
private ItemFeign itemFeign;

再调用库存递减方法

 //减少库存  调用goods 微服务的 feign 减少库存
itemFeign.decrCount(order.getUserId());

注意:调用减少库存方法,要放置在清除redis购物车数据之前。

完整代码如下:

 /**
     * 增加Order
     * @param order
     */
    @Override
    public void add(Order order){
        // 得到购物车数据
        List<Cart> cartList = (List<Cart>) redisTemplate.boundHashOps("cartList").get(order.getUserId());
        for (Cart cart : cartList) {
            long orderId = idWorker.nextId();
            System.out.println("sellerId:" + cart.getSellerId());
            Order tborder = new Order();// 新创建订单对象
            //复制前端传递过来的订单的属性值,到新创建订单对象
            BeanUtils.copyProperties(order,tborder);
            tborder.setOrderId(orderId);// 订单ID(一定要在复制属性后设置)
            tborder.setStatus("1");// 状态:未付款
            tborder.setCreateTime(new Date());// 订单创建日期
            tborder.setUpdateTime(new Date());// 订单更新日期
            tborder.setSellerId(cart.getSellerId());// 商家ID
            // 循环购物车明细
            double money = 0;
            for (OrderItem orderItem : cart.getOrderItemList()) {
                orderItem.setId(idWorker.nextId());
                orderItem.setOrderId(orderId);// 订单ID
                orderItem.setSellerId(cart.getSellerId());
                money += orderItem.getTotalFee().doubleValue();// 金额累加
                //保存购物明细
                orderItemMapper.insert(orderItem);
            }
            tborder.setPayment(new BigDecimal(money));
            //保存订单
            this.save(tborder);
        }

        //******************减少库存********************************
        //减少库存  调用goods 微服务的 feign 减少库存
        itemFeign.decrCount(order.getUserId());

        redisTemplate.boundHashOps("cartList").delete(order.getUserId());


    }

需要设置开启Feign熔断、设置熔断时间、连接超时时间

ribbon:
  ReadTimeout: 300000
#hystrix 配置
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 10000
          strategy: SEMAPHORE
feign:
  hystrix:
    enabled: true

3.3.4 测试

库存减少前,查询数据库Sku数据如下:个数9999,销量0

使用Postman执行 http://localhost:8001/api/order/

执行测试后,剩余库存9998,销量1

3.4 增加积分[学员完成]

比如每次下单完成之后,给用户增加10个积分,支付完成后赠送优惠券,优惠券可用于支付时再次抵扣。我们先完成增加积分功能。如tb_user表:points表示用户积分

3.4.1 代码实现

(1)dao层

修改dongyimai-user-service微服务的com.offcn.user.dao.UserMapper接口,增加用户积分方法,代码如下:

/***
 * 增加用户积分
 * @param username
 * @param point
 * @return
 */
@Update("UPDATE tb_user SET points=points+#{point} WHERE  username=#{username}")
int addUserPoints(@Param("username") String username, @Param("point") Integer point);

(2)业务层

修改dongyimai-user-service微服务的com.offcn.user.service.UserService接口,代码如下:

/***
 * 添加用户积分
 * @param username
 * @param point
 * @return
 */
int addUserPoints(String username,Integer point);

修改dongyimai-user-service微服务的com.offcn.user.service.impl.UserServiceImpl,增加添加积分方法实现,代码如下:

@Autowired
private UserMapper userMapper;

/***
 * 添加用户积分
 * @param username
 * @param point
 * @return
 */
@Override
public int addUserPoints(String username, Integer point) {
    return userMapper.addUserPoints(username,point);
}

(3)控制层

修改dongyimai-user-service微服务的com.offcn.user.controller.UserController,添加增加用户积分方法,代码如下:

@Autowired
private TokenDecode tokenDecode;

/***
 * 增加用户积分
 * @param points:要添加的积分
 */
@GetMapping(value = "/points/add")
public Result addPoints(Integer points){
    //获取用户名
    Map<String, String> userMap = tokenDecode.getUserInfo();
    String username = userMap.get("username");

    //添加积分
    userService.addUserPoints(username,points);
    return new Result(true,StatusCode.OK,"添加积分成功!");
}

(4)Feign添加

修改dongyimai-user-service-api工程,修改com.offcn.user.feign.UserFeign,添加增加用户积分方法,代码如下:

/***
 * 添加用户积分
 * @param points
 * @return
 */
@GetMapping(value = "/points/add")
Result addPoints(@RequestParam(value = "points")Integer points);

3.4.2 增加积分调用

修改dongyimai-order-service,添加dongyimai-user-service-api的依赖,修改pom.xml,添加如下依赖:

<!--user api 依赖-->
<dependency>
    <groupId>com.offcn</groupId>
    <artifactId>dongyimai-user-service-api</artifactId>
    <version>1.0</version>
</dependency>

在增加订单的时候,同时添加用户积分,修改dongyimai-order-service微服务的com.offcn.order.service.impl.OrderServiceImpl下单方法,增加调用添加积分方法,代码如下:

修改dongyimai-order-service的启动类com.offcn.OrderApplication,添加feign的包路径:

//注意要设置tb_user表的每个用户的积分初始值为0


avatar
青山
悟已往之不谏 知来者之可追
一言
今日诗词
站点信息
本站访客数 :
本站总访问量 :