第十三章
提交订单
优就业.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()">
登 录
</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