1.构建页面环境
1.1 导入静态资源nginx
等待付款 --------->detail
订单页 --------->list
结算页 --------->confirm
收银页 ---------> pay

1.2 配置host
# gulimall 192.168.157.128 gulimall.com # search 192.168.157.128 search.gulimall.com # item 商品详情 192.168.157.128 item.gulimall.com #商城认证 192.168.157.128 auth.gulimall.com #购物车 192.168.157.128 cart.gulimall.com #订单 192.168.157.128 order.gulimall.com #单点登录 127.0.0.1 ssoserver.com 127.0.0.1 client1.com 127.0.0.1 client2.com
1.3 配置网关
gulimall-gateway/src/main/resources/application.yml
#订单 - id: gulimall_order_route uri: lb://gulimall-order predicates: - Host=order.gulimall.com
1.4 打开注册发现
@EnableDiscoveryClient
1.5 新增依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
1.6 修改每个页面的静态资源路径
src=" ===>src="/static/order/xxx/
herf=" ===>herf="/static/order/xxx/
1.7 测试
1.7.1 订单确认页
确认页面前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/confirm.html
order.gulimall.com/confirm.html
1.7.2 订单列表页
订单列表页面前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/list.html
谷粒商城订单 (gulimall.com)
1.7.3 订单详情页
订单详情页前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/detail.html
order.gulimall.com/detail.html
1.7.4 订单支付页
订单支付页面前端代码:https://gitee.com/zhourui815/gulimall/blob/master/gulimall-order/src/main/resources/templates/pay.html
order.gulimall.com/pay.html
2. 整合Spring Session
2.1 导入依赖
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> <!--redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <!--jedis,redis客户端--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
2.2 开启Spring Session
@EnableRedisHttpSession //整合Redis作为session存储
2.3 配置Spring Session存储方式
redis:
host: 192.168.157.128
session:
store-type: redis
2.4 SpringSession 自定义
package site.zhourui.gulimall.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/** * @author zr * @date 2021/12/12 10:29 */
@Configuration
public class GulimallSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
//放大作用域
cookieSerializer.setDomainName("gulimall.com");
cookieSerializer.setCookieName("GULISESSION");
cookieSerializer.setCookieMaxAge(60*60*24*7);
return cookieSerializer;
}
//session存储对象方式json,默认jdk
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
2.5 整合后效果
可以实现登录成功后用户信息共享
3. 整合线程池
3.1 自定义线程池配置
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/MyThreadConfig.java
package site.zhourui.gulimall.order.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/** * @author zr * @date 2021/11/28 10:12 */
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
return new ThreadPoolExecutor(
pool.getCoreSize(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(100000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/ThreadPoolConfigProperties.java
package site.zhourui.gulimall.order.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@ConfigurationProperties(prefix = "gulimall.thread")
@Component
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
}
3.2 配置
gulimall:
thread:
core-size: 20
max-size: 200
keep-alive-time: 10
4. 订单中心(理论)
电商系统涉及到 3 流, 分别时信息流, 资金流, 物流, 而订单系统作为中枢将三者有机的集合起来。订单模块是电商系统的枢纽, 在订单这个环节上需求获取多个模块的数据和信息, 同时对这些信息进行加工处理后流向下个环节, 这一系列就构成了订单的信息流通。
4.1 订单的构成
4.1.1 用户信息
用户信息包括用户账号、 用户等级、 用户的收货地址、 收货人、 收货人电话等组成, 用户账户需要绑定手机号码, 但是用户绑定的手机号码不一定是收货信息上的电话。 用户可以添加多个收货信息, 用户等级信息可以用来和促销系统进行匹配, 获取商品折扣, 同时用户等级还可以获取积分的奖励等
4.1.2 订单基础信息
订单基础信息是订单流转的核心, 其包括订单类型、 父/子订单、 订单编号、 订单状态、 订单流转的时间等。
(1) 订单类型包括实体商品订单和虚拟订单商品等, 这个根据商城商品和服务类型进行区分。 (2) 同时订单都需要做父子订单处理, 之前在初创公司一直只有一个订单, 没有做父子订单处理后期需要进行拆单的时候就比较麻烦, 尤其是多商户商场, 和不同仓库商品的时候,父子订单就是为后期做拆单准备的。 (3) 订单编号不多说了, 需要强调的一点是父子订单都需要有订单编号, 需要完善的时候可以对订单编号的每个字段进行统一定义和诠释。 (4) 订单状态记录订单每次流转过程, 后面会对订单状态进行单独的说明。 (5) 订单流转时间需要记录下单时间, 支付时间, 发货时间, 结束时间/关闭时间等等
4.1.3 商品信息
商品信息从商品库中获取商品的 SKU 信息、 图片、 名称、 属性规格、 商品单价、 商户信息等, 从用户下单行为记录的用户下单数量, 商品合计价格等。
4.1.4 优惠信息
优惠信息记录用户参与的优惠活动, 包括优惠促销活动, 比如满减、 满赠、 秒杀等, 用户使用的优惠券信息, 优惠券满足条件的优惠券需要默认展示出来, 具体方式已在之前的优惠券篇章做过详细介绍, 另外还虚拟币抵扣信息等进行记录。
4.1.4.1为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目, 而支付信息需要加入数据进行计算, 所以做为区分。
4.1.5 支付信息
( 1) 支付流水单号, 这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支付流水号, 财务通过订单号和流水单号与支付通道进行对账使用。 ( 2) 支付方式用户使用的支付方式, 比如微信支付、 支付宝支付、 钱包支付、 快捷支付等。支付方式有时候可能有两个——余额支付+第三方支付。 ( 3) 商品总金额, 每个商品加总后的金额; 运费, 物流产生的费用; 优惠总金额, 包括促销活动的优惠金额, 优惠券优惠金额, 虚拟积分或者虚拟币抵扣的金额, 会员折扣的金额等之和; 实付金额, 用户实际需要付款的金额。用户实付金额=商品总金额+运费-优惠总金额
4.1.6 物流信息
物流信息包括配送方式, 物流公司, 物流单号, 物流状态, 物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。
4.2 订单状态
- 待付款 用户提交订单后, 订单进行预下单, 目前主流电商网站都会唤起支付, 便于用户快速完成支付, 需要注意的是待付款状态下可以对库存进行锁定, 锁定库存需要配置支付超时时间, 超时后将自动取消订单, 订单变更关闭状态。
- 已付款/待发货 用户完成订单支付, 订单系统需要记录支付时间, 支付流水单号便于对账, 订单下放到 WMS系统, 仓库进行调拨, 配货, 分拣, 出库等操作。
- 待收货/已发货 仓储将商品出库后, 订单进入物流环节, 订单系统需要同步物流信息, 便于用户实时知悉物品物流状态
- 已完成 用户确认收货后, 订单交易完成。 后续支付侧进行结算, 如果订单存在问题进入售后状态
- 已取消 付款之前取消订单。 包括超时未付款或用户商户取消订单都会产生这种订单状态。
- 售后中 用户在付款后申请退款, 或商家发货后用户申请退换货。售后也同样存在各种状态, 当发起售后申请后生成售后订单, 售后订单状态为待审核, 等待商家审核, 商家审核通过后订单状态变更为待退货, 等待用户将商品寄回, 商家收货后订单状态更新为待退款状态, 退款到用户原账户后订单状态更新为售后成功。
4.3 订单流程
订单流程是指从订单产生到完成整个流转的过程, 从而行程了一套标准流程规则。 而不同的产品类型或业务类型在系统中的流程会千差万别, 比如上面提到的线上实物订单和虚拟订单的流程, 线上实物订单与 O2O 订单等, 所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程, 对应的场景就是购买商品和退换货流程, 正向流程就是一个正常的网购步骤: 订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后, 订单是如何在多系统之间交互流转的, 可概括如下图
4.3.1 订单创建与支付 (重点)
- 订单创建前需要预览订单, 选择收货信息等
- 订单创建需要锁定库存, 库存有才可创建, 否则不能创建
- 订单创建后超时未支付需要解锁库存
- 支付成功后, 需要进行拆单, 根据商品打包方式, 所在仓库, 物流等进行拆单
- 支付的每笔流水都需要记录, 以待查账
- 订单创建, 支付成功等状态都需要给 MQ 发送消息, 方便其他系统感知订阅
4.3.2 逆向流程
- 修改订单, 用户没有提交订单, 可以对订单一些信息进行修改, 比如配送信息,优惠信息, 及其他一些订单可修改范围的内容, 此时只需对数据进行变更即可。
- 订单取消, 用户主动取消订单和用户超时未支付, 两种情况下订单都会取消订单, 而超时情况是系统自动关闭订单, 所以在订单支付的响应机制上面要做支付的限时处理, 尤其是在前面说的下单减库存的情形下面, 可以保证快速的释放库存。另外需要需要处理的是促销优惠中使用的优惠券, 权益等视平台规则, 进行相应补回给用户。
- 退款, 在待发货订单状态下取消订单时, 分为缺货退款和用户申请退款。 如果是全部退款则订单更新为关闭状态, 若只是做部分退款则订单仍需进行进行, 同时生成一条退款的售后订单, 走退款流程。 退款金额需原路返回用户的账户。
- 发货后的退款, 发生在仓储货物配送, 在配送过程中商品遗失, 用户拒收, 用户收货后对商品不满意, 这样情况下用户发起退款的售后诉求后, 需要商户进行退款的审核, 双方达成一致后, 系统更新退款状态, 对订单进行退款操作, 金额原路返回用户的账户, 同时关闭原订单数据。 仅退款情况下暂不考虑仓库系统变化。 如果发生双方协调不一致情况下, 可以申请平台客服介入。 在退款订单商户不处理的情况下, 系统需要做限期判断, 比如 5 天商户不处理, 退款单自动变更同意退款。
5. 订单中心(代码)
5.1 订单登录拦截
因为订单系统必然涉及到用户信息,因此进入订单系统的请求必须是已经登录的,所以我们需要通过拦截器对未登录订单请求进行拦截
gulimall-order/src/main/java/site/zhourui/gulimall/order/interceptor/LoginUserInterceptor.java
package site.zhourui.gulimall.order.interceptor;
/** * @author zr * @date 2021/12/21 22:04 */
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import site.zhourui.common.constant.AuthServerConstant;
import site.zhourui.common.vo.MemberResponseVo;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.PrintWriter;
import static site.zhourui.common.constant.AuthServerConstant.LOGIN_USER;
/** * 登录拦截器 * 从session中获取了登录信息(redis中),封装到了ThreadLocal中 */
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
MemberResponseVo memberResponseVo = (MemberResponseVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (memberResponseVo != null) {
loginUser.set(memberResponseVo);
return true;
}else {
session.setAttribute("msg","请先登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/config/OrderWebConfig.java
package site.zhourui.gulimall.order.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import site.zhourui.gulimall.order.interceptor.LoginUserInterceptor;
/** * @author zr * @date 2021/12/21 22:05 */
@Configuration
public class OrderWebConfig implements WebMvcConfigurer {
@Autowired
private LoginUserInterceptor loginUserInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginUserInterceptor).addPathPatterns("/**");
}
}
5.2 订单确认页
5.2.1 模型抽取
确认页提交数据
gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderConfirmVo.java
package site.zhourui.gulimall.order.vo;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/** * 订单确认页需要用的数据 * @author zr * @date 2021/12/21 22:22 */
public class OrderConfirmVo {
@Getter
@Setter
List<MemberAddressVo> memberAddressVos;/** 会员收获地址列表 **/
@Getter @Setter
List<OrderItemVo> items; /** 所有选中的购物项【购物车中的所有项】 **/
@Getter @Setter
private Integer integration;/** 优惠券(会员积分) **/
/** TODO 防止重复提交的令牌 幂等性**/
@Getter @Setter
private String orderToken;
@Getter @Setter
Map<Long,Boolean> stocks;
public Integer getCount() {
Integer count = 0;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
count += item.getCount();
}
}
return count;
}
/** 总商品金额 **/
//BigDecimal total;
//计算订单总额
public BigDecimal getTotal() {
BigDecimal totalNum = BigDecimal.ZERO;
if (items != null && items.size() > 0) {
for (OrderItemVo item : items) {
//计算当前商品的总价格
BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
//再计算全部商品的总价格
totalNum = totalNum.add(itemPrice);
}
}
return totalNum;
}
/** 应付总额 **/
//BigDecimal payPrice;
public BigDecimal getPayPrice() {
return getTotal();
}
}
确认页提交数据模型还需要地址信息
gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/MemberAddressVo.java
package site.zhourui.gulimall.order.vo;
import lombok.Data;
/** * 地址信息 * @author zr * @date 2021/12/21 22:24 */
@Data
public class MemberAddressVo {
private Long id;
/** * member_id */
private Long memberId;
/** * 收货人姓名 */
private String name;
/** * 电话 */
private String phone;
/** * 邮政编码 */
private String postCode;
/** * 省份/直辖市 */
private String province;
/** * 城市 */
private String city;
/** * 区 */
private String region;
/** * 详细地址(街道) */
private String detailAddress;
/** * 省市区代码 */
private String areacode;
/** * 是否默认 */
private Integer defaultStatus;
}
确认页提交数据模型还需要订单行信息
gulimall-order/src/main/java/site/zhourui/gulimall/order/vo/OrderItemVo.java
package site.zhourui.gulimall.order.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/** * 购物项内容 * @author zr * @date 2021/12/21 22:23 */
@Data
public class OrderItemVo {
private Long skuId; // skuId
private Boolean check = true; // 是否选中
private String title; // 标题
private String image; // 图片
private List<String> skuAttrValues;// 商品销售属性
private BigDecimal price; // 单价
private Integer count; // 当前商品数量
private BigDecimal totalPrice; // 总价
private BigDecimal weight = new BigDecimal("0.085");// 商品重量
}
5.2.2 提交确认订单
5.2.2.1 订单确认页流程
1、远程调用:获取所有收货地址【member-ums表】 2、远程调用:所有选中的商品(最新价格-远程调用)【cart-redis中】【product-查询最新价格】 3、查询用户积分【session的用户信息中】 4、订单总额【根据所有选中的价格之和 求得】 5、应付总额【暂时跟订单总额相等】【优惠卡等功能不做,直接用积分】
6、查询每个商品是否有货【批量查询ware服务】 7、收货地址高亮【选中地址调用ajax直接远程调用ware计算运费【远程调用会员服务member传入addrId获取详细地址】 WareInfoController /fare 接口返回运费信息,和地址信息 8、防重令牌【防止用户多次 提交订单】【点击提交订单后,数据库只保存一条订单信息(幂等性,提交1次和多次结果是一致的)】
5.2.2.2 去到订单确认页面
返回订单确认页所需要的数据OrderConfirmVo
gulimall-order/src/main/java/site/zhourui/gulimall/order/web/OrderWebController.java
/** * 去结算确认页 * @param model * @param request * @return * @throws ExecutionException * @throws InterruptedException */
@GetMapping(value = "/toTrade")
public String toTrade(Model model, HttpServletRequest request) throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = orderService.confirmOrder();
model.addAttribute("confirmOrderData",confirmVo);
//展示订单确认的数据
return "confirm";
}
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/OrderService.java
OrderConfirmVo confirmOrder();
gulimall-order/src/main/java/site/zhourui/gulimall/order/service/impl/OrderServiceImpl.java
- 判断用户登录信息
- 远程查询所有的收获地址列表
- 远程查询购物车所有选中的购物项
- 远程查询商品库存信息
- 查询用户积分
- 价格数据自动计算
- 防重令牌(防止表单重复提交)
/** * 订单确认页返回需要用的数据 * @return */ @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { //构建OrderConfirmVo OrderConfirmVo confirmVo = new OrderConfirmVo(); //获取当前用户登录的信息 MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get()