尚硅谷_尚品甄选_第6章_订单.md 47 KB

[TOC]

[TOC]

[TOC]

订单

1 结算

1.1 需求说明

入口:购物车点击去结算按钮 ,进入结算页面(订单确认页面),如图所示:

image-20240727072230364

分析页面需要的数据:

1、 用户地址信息列表管理(增删改查),结算页选中默认地址

2、 购物车中选择的商品列表,及商品的总金额

查看接口文档:

用户地址信息接口地址及返回结果:

#用户地址列表
get /user/userAddress/list
返回结果:
{
    "msg": "操作成功",
    "code": 200,
    "data": [
        {
            "id": 60,
            "userId": 1,
            "name": "晴天",
            "phone": "15023656352",
            "tagName": "家",
            "provinceCode": "110000",
            "cityCode": "110100",
            "districtCode": "110101",
            "address": "东直门1号",
            "fullAddress": "北京市北京市东城区东直门1号",
            "isDefault": 1
        },
        ...
    ]
}
 
#添加用户地址
post /user/userAddress
参数:
{
    "id": null,
    "name": "cs",
    "phone": "15090909090",
    "provinceCode": "110000",
    "cityCode": "110100",
    "districtCode": "110102",
    "address": "111",
    "tagName": "家",
    "isDefault": 0
}     
返回结果:
{
    "msg": "操作成功",
    "code": 200
}
        
#修改用户地址
put /user/userAddress
参数:
{
    "id": 60
    "name": "cs",
    "phone": "15090909090",
    "provinceCode": "110000",
    "cityCode": "110100",
    "districtCode": "110102",
    "address": "111",
    "tagName": "家",
    "isDefault": 0
}     
返回结果:
{
    "msg": "操作成功",
    "code": 200
}     
        
#删除用户地址
delete /user/userAddress/{id}
返回结果:
{
    "msg": "操作成功",
    "code": 200
}

结算接口地址及返回结果:

get /order/orderInfo/trade
返回结果:
{
    "msg": "操作成功",
    "code": 200,
    "data": {
        "totalAmount": 8998.00,
        "orderItemList": [
            {
                "orderId": null,
                "skuId": 9,
                "skuName": "华为笔记本 32G",
                "thumbImg": "http://139.198.127.41:9000/spzx/20230525/c8f2eae0d36b6270.jpg.avif",
                "skuPrice": 5999.00,
                "skuNum": 1
            },
            ...
        ],
        "tradeNo": "1d76f36b59414e869e843fc742e21469"
    }
}

1.2 地区管理

操作模块:spzx-user

1.2.1 RegionController

添加如下方法:

@Operation(summary = "查询地区信息树形列表")
@GetMapping(value = "/treeSelect/{parentCode}")
public AjaxResult treeSelect(@PathVariable String parentCode) {
    return success(regionService.treeSelect(parentCode));
}

1.2.2 IRegionService

List<Region> treeSelect(String parentCode);

1.2.3 RegionServiceImpl

@Override
public List<Region> treeSelect(String parentCode) {
    return regionMapper.selectList(new LambdaQueryWrapper<Region>().eq(Region::getParentCode, parentCode));
}

1.3 搭建订单管理模块

1.3.1 新建模块

在spzx-modules模块下新建子模块spzx-order

1.3.2 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>com.spzx</groupId>
        <artifactId>spzx-modules</artifactId>
        <version>3.6.3</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spzx-order</artifactId>

    <description>
        spzx-order订单模块
    </description>

    <dependencies>
        <!-- SpringCloud Alibaba Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Nacos Config -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!-- SpringCloud Alibaba Sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

        <!-- SpringBoot Actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- Mysql Connector -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
        </dependency>

        <!-- RuoYi Common DataScope -->
        <dependency>
            <groupId>com.spzx</groupId>
            <artifactId>spzx-common-datascope</artifactId>
        </dependency>

        <!-- RuoYi Common Log -->
        <dependency>
            <groupId>com.spzx</groupId>
            <artifactId>spzx-common-log</artifactId>
        </dependency>

    </dependencies>

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>17</source>
                    <target>17</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

1.3.3 banner.txt

在resources目录下新建banner.txt

Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
                            _                           _                    
                           (_)                         | |                   
 _ __  _   _   ___   _   _  _  ______  ___  _   _  ___ | |_   ___  _ __ ___  
| '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \ 
| |   | |_| || (_) || |_| || |        \__ \| |_| |\__ \| |_ |  __/| | | | | |
|_|    \__,_| \___/  \__, ||_|        |___/ \__, ||___/ \__| \___||_| |_| |_|
                      __/ |                  __/ |                           
                     |___/                  |___/                            

1.3.4 bootstrap.yml

在resources目录下新建bootstrap.yml

# Tomcat
server:
  port: 9207

# Spring
spring:
  application:
    # 应用名称
    name: spzx-order
  profiles:
    # 环境配置
    active: dev
  main:
    allow-bean-definition-overriding: true #当遇到同样名字的时候,是否允许覆盖注册
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 192.168.200.131:8848
      config:
        # 配置中心地址
        server-addr: 192.168.200.131:8848
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

1.3.5 spzx-order-dev.yml

在nacos上添加商品服务配置文件

mybatis-plus:
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  type-aliases-package: com.spzx.**.domain
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 查看日志
  global-config:
    db-config:
      logic-delete-field: del_flag # 全局逻辑删除的实体字段名
      logic-delete-value: 2 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
# spring配置
spring:
  data:
    redis:
      host: 192.168.200.131
      port: 6379
      password:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.200.131:3306/spzx-order?characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    hikari:
      connection-test-query: SELECT 1
      connection-timeout: 60000
      idle-timeout: 500000
      max-lifetime: 540000
      maximum-pool-size: 10
      minimum-idle: 5
      pool-name: GuliHikariPool

1.3.6 logback.xml

在resources目录下新建logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!-- 日志存放路径 -->
   <property name="log.path" value="logs/spzx-order" />
   <!-- 日志输出格式 -->
   <property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />

    <!-- 控制台输出 -->
   <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>${log.pattern}</pattern>
      </encoder>
   </appender>

    <!-- 系统日志输出 -->
   <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
       <file>${log.path}/info.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
      <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
         <fileNamePattern>${log.path}/info.%d{yyyy-MM-dd}.log</fileNamePattern>
         <!-- 日志最大的历史 60天 -->
         <maxHistory>60</maxHistory>
      </rollingPolicy>
      <encoder>
         <pattern>${log.pattern}</pattern>
      </encoder>
      <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>INFO</level>
            <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
            <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
   </appender>

    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
       <file>${log.path}/error.log</file>
        <!-- 循环政策:基于时间创建日志文件 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志文件名格式 -->
            <fileNamePattern>${log.path}/error.%d{yyyy-MM-dd}.log</fileNamePattern>
         <!-- 日志最大的历史 60天 -->
         <maxHistory>60</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!-- 过滤的级别 -->
            <level>ERROR</level>
         <!-- 匹配时的操作:接收(记录) -->
            <onMatch>ACCEPT</onMatch>
         <!-- 不匹配时的操作:拒绝(不记录) -->
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 系统模块日志级别控制  -->
   <logger name="com.spzx" level="info" />
   <!-- Spring日志级别控制  -->
   <logger name="org.springframework" level="warn" />

   <root level="info">
      <appender-ref ref="console" />
   </root>

   <!--系统操作日志-->
    <root level="info">
        <appender-ref ref="file_info" />
        <appender-ref ref="file_error" />
    </root>
</configuration>

1.3.7 SpzxOrderApplication

添加启动类

package com.spzx.order;

/**
 * 会员模块
 *
 */
@EnableCustomConfig
@EnableRyFeignClients
@SpringBootApplication
public class SpzxOrderApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(SpzxOrderApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  系统模块启动成功   ლ(´ڡ`ლ)゙  \n" +
                " .-------.       ____     __        \n" +
                " |  _ _   \\      \\   \\   /  /    \n" +
                " | ( ' )  |       \\  _. /  '       \n" +
                " |(_ o _) /        _( )_ .'         \n" +
                " | (_,_).' __  ___(_ o _)'          \n" +
                " |  |\\ \\  |  ||   |(_,_)'         \n" +
                " |  | \\ `'   /|   `-'  /           \n" +
                " |  |  \\    /  \\      /           \n" +
                " ''-'   `'-'    `-..-'              ");
    }
}

1.3.8 配置网关

在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息

        # 订单服务
        - id: spzx-order
          uri: lb://spzx-order
          predicates:
            - Path=/order/**
          filters:
            - StripPrefix=1

1.3.9 生成代码

1 复制一个代码生成器

从其他模块中f复制GenMP,修改代码生成器中关于服务名的部分,配置需要生成的表

2 复制mapper到resources
3 OrderInfo

补充属性

@TableField(exist = false)
private List<OrderItem> orderItemList;

orderStatus的数据类型改成Integer

1.4 获取选中购物项数据接口

1.4.1 远程调用接口开发

操作模块:spzx-cart

1 CartController
@Operation(summary = "查询用户购物车列表中选中商品列表")
@InnerAuth
@GetMapping("/getCartCheckedList")
public R<List<CartInfo>> getCartCheckedList() {

    return R.ok(cartService.getCartCheckedList());
}
2 ICartService
List<CartInfo> getCartCheckedList();
3 CartServiceImpl
@Override
public List<CartInfo> getCartCheckedList() {

    Long userId = SecurityContextHolder.getUserId();
    String cartKey = this.getCartKey(userId);
    List<CartInfo> cartCachInfoList = redisTemplate.opsForHash().values(cartKey);
    List<CartInfo> cartInfoList = new ArrayList<>();
    if (!CollectionUtils.isEmpty(cartCachInfoList)) {

        cartInfoList = cartCachInfoList
            .stream()
            .filter(item -> item.getIsChecked().intValue() == 1)
            .collect(Collectors.toList());
    }
    return cartInfoList;
}

1.4.2 openFeign接口定义

操作模块:spzx-api-cart

1 RemoteCartService
package com.spzx.cart.api;

@FeignClient(
    value = ServiceNameConstants.CART_SERVICE, 
    fallbackFactory = RemoteCartFallbackFactory.class)
public interface RemoteCartService
{

    @GetMapping("/getCartCheckedList")
    R<List<CartInfo>> getCartCheckedList(
        @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}
2 ServiceNameConstants
/**
 * 购物车服务的serviceid
 */
public static final String CART_SERVICE = "spzx-cart";
3 RemoteCartFallbackFactory
package com.spzx.cart.api.factory;


/**
 * 购物车降级处理
 *
 * @author atguigu
 */
@Component
@Slf4j
public class RemoteCartFallbackFactory implements FallbackFactory<RemoteCartService>
{
    @Override
    public RemoteCartService create(Throwable throwable)
    {
        log.error("购物车服务调用失败:{}", throwable.getMessage());
        return new RemoteCartService()
        {

            @Override
            public R<List<CartInfo>> getCartCheckedList(String source) {
                return R.fail("获取用户购物车选中数据失败:" + throwable.getMessage());
            }

        };
    }
}
4 加载配置类

resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.spzx.cart.api.factory.RemoteCartFallbackFactory

1.5 后端业务接口

操作模块:spzx-order

1.5.1 TradeVo

package com.spzx.order.vo;

@Data
@Schema(description = "结算实体类")
public class TradeVo {

    @Schema(description = "结算总金额")
    private BigDecimal totalAmount;

    @Schema(description = "结算商品列表")
    private List<OrderItem> orderItemList;

    @Schema(description = "交易号")
    private String tradeNo;
    
    @Schema(description = "是否是立即购买")
    private Boolean isBuy = false;
}

1.5.2 OrderInfoController

package com.spzx.order.controller;

@Tag(name = "订单管理")
@RestController
@RequestMapping("/orderInfo")
public class OrderInfoController extends BaseController {

    @Autowired
    private IOrderInfoService orderInfoService;

    @Operation(summary = "订单结算")
    @GetMapping("/trade")
    public AjaxResult orderTradeData() {
        return success(orderInfoService.orderTradeData());
    }
}

1.5.3 IOrderInfoService

TradeVo orderTradeData();

1.5.4 OrderInfoServiceImpl

@Autowired
private RemoteCartService remoteCartService;

@Autowired
private RedisTemplate redisTemplate;

@Override
public TradeVo orderTradeData() {
    // 获取当前登录用户的id
    Long userId = SecurityContextHolder.getUserId();

    R<List<CartInfo>> cartInfoListResult = remoteCartService.getCartCheckedList(SecurityConstants.INNER);
    if (R.FAIL == cartInfoListResult.getCode()) {
        throw new ServiceException(cartInfoListResult.getMsg());
    }
    List<CartInfo> cartInfoList = cartInfoListResult.getData();
    if (CollectionUtils.isEmpty(cartInfoList)) {
        throw new ServiceException("购物车无选中商品");
    }

    //将集合泛型从购物车改为订单明细
    List<OrderItem> orderItemList = null;
    BigDecimal totalAmount = new BigDecimal(0);
    if (!CollectionUtils.isEmpty(cartInfoList)) {
        orderItemList = cartInfoList.stream().map(cartInfo -> {
            OrderItem orderItem = new OrderItem();
            BeanUtils.copyProperties(cartInfo, orderItem);
            orderItem.setSkuNum(cartInfo.getSkuNum());
            return orderItem;
        }).collect(Collectors.toList());

        //订单总金额
        for (OrderItem orderItem : orderItemList) {
            totalAmount = totalAmount.add(orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum())));
        }
    }

    //渲染订单确认页面-生成用户流水号(防止页面重复提交和页面等待超时)
    String tradeNo = this.generateTradeNo(userId);

    TradeVo tradeVo = new TradeVo();
    tradeVo.setTotalAmount(totalAmount);
    tradeVo.setOrderItemList(orderItemList);
    tradeVo.setTradeNo(tradeNo);
    return tradeVo;
}

/**
 * 渲染订单确认页面-生成用户流水号
 *
 * @param userId
 * @return
 */
private String generateTradeNo(Long userId) {
    //1.构建流水号Key
    String userTradeKey = "user:tradeNo:" + userId;
    //2.构建流水号value
    String tradeNo = UUID.randomUUID().toString().replaceAll("-", "");
    //3.将流水号存入Redis 暂存5分钟
    redisTemplate.opsForValue().set(userTradeKey, tradeNo, 5, TimeUnit.MINUTES);
    return tradeNo;
}

2 下单

2.1 需求说明

需求说明:用户在结算页面点击提交订单按钮,那么此时就需要保存订单信息(order_info)、订单项信息(order_item)及记录订单日志(order_log),下单成功重定向到订单支付页面

查看接口文档:

下单接口地址及返回结果:

post /order/orderInfo/submitOrder
参数:
{
    "orderItemList": [
        {
            "skuId": 6,
            "skuName": "小米 红米Note10 5G手机 颜色:黑色 内存:18G",
            "thumbImg": "http://139.198.127.41:9000/spzx/20230525/665832167-1_u_1.jpg",
            "skuPrice": 2999,
            "skuNum": 1
        },
        ...
    ],
    "userAddressId": 2,
    "feightFee": 0,
    "remark": "赶快发货"
}
返回结果(订单id):
{
    "code": 200,
    "message": "操作成功",
    "data": 1
}

2.2 获取商品最新价格接口

2.2.1 远程调用接口开发

操作模块:spzx-product

ProductSkuController
@Operation(summary = "获取商品sku最新价格信息")
@InnerAuth
@GetMapping(value = "/getSkuPrice/{skuId}")
public R<SkuPriceVo> getSkuPrice(@PathVariable("skuId") Long skuId)
{
    return R.ok(productSkuService.getSkuPrice(skuId));
}

2.2.2 openFeign接口定义

操作模块:spzx-api-product

1 移动实体类

将spzx-product模块SkuPriceVo类移动到spzx-api-product模块

2 RemoteProductService
@GetMapping(value = "/productSku/getSkuPrice/{skuId}")
R<SkuPriceVo> getSkuPrice(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
3 RemoteProductFallbackFactory
@Override
public R<SkuPriceVo> getSkuPrice(Long skuId, String source) {
    return R.fail("获取商品sku价格失败:" + throwable.getMessage());
}

2.3 更新购物车最新价格

操作模块:spzx-cart

2.3.1 远程调用接口开发

1 CartController
@Operation(summary="更新用户购物车列表价格")
@InnerAuth
@GetMapping("/updateCartPrice/{userId}")
public R<Boolean> updateCartPrice(@PathVariable("userId") Long userId){
    return R.ok(cartService.updateCartPrice(userId));
}
2 ICartService
Boolean updateCartPrice(Long userId);
3 CartServiceImpl
@Override
public Boolean updateCartPrice(Long userId) {
    String cartKey = getCartKey(userId);
    BoundHashOperations<String, String, CartInfo> hashOperations = redisTemplate.boundHashOps(cartKey);
    List<CartInfo> cartCachInfoList = hashOperations.values();
    if (!CollectionUtils.isEmpty(cartCachInfoList)) {
        for (CartInfo cartInfo : cartCachInfoList) {
            if (cartInfo.getIsChecked().intValue() == 1) {
                
                SkuPriceVo skuPrice = remoteProductService
                    .getSkuPrice(cartInfo.getSkuId(), SecurityConstants.INNER)
                    .getData();
                
                //放入购物车时价格
                cartInfo.setCartPrice(skuPrice.getSalePrice());

                //实时价格
                cartInfo.setSkuPrice(skuPrice.getSalePrice());
                hashOperations.put(cartInfo.getSkuId().toString(), cartInfo);
            }
        }
    }
    return true;
}

2.3.2 openFeign接口定义

操作模块:spzx-api-cart

1 RemoteCartService
@GetMapping("/updateCartPrice/{userId}")
R<Boolean> updateCartPrice(
    @PathVariable("userId") Long userId, 
    @RequestHeader(SecurityConstants.FROM_SOURCE) String source
);
2 RemoteCartFallbackFactory
@Override
public R<Boolean> updateCartPrice(Long userId, String source) {
    return R.fail("更新购物车价格失败:" + throwable.getMessage());
}

2.4 删除购物车选中商品

下单成功后,删除购物车选中的商品

操作模块:spzx-cart

2.4.1 远程调用接口开发

1 CartController
@Operation(summary="删除用户购物车列表中选中商品列表")
@InnerAuth
@GetMapping("/deleteCartCheckedList/{userId}")
public R<Boolean> deleteCartCheckedList(@PathVariable("userId") Long userId){
    return R.ok(cartService.deleteCartCheckedList(userId));
}
2 ICartService
 Boolean deleteCartCheckedList(Long userId);
3 CartServiceImpl
@Override
public Boolean deleteCartCheckedList(Long userId) {
    String cartKey = getCartKey(userId);
    BoundHashOperations<String, String, CartInfo> hashOperations = redisTemplate.boundHashOps(cartKey);
    List<CartInfo> cartCachInfoList = hashOperations.values();
    if (!CollectionUtils.isEmpty(cartCachInfoList)) {
        for (CartInfo cartInfo : cartCachInfoList) {
            // 删除选中的商品
            if (cartInfo.getIsChecked().intValue() == 1) {
                hashOperations.delete(cartInfo.getSkuId().toString());
            }
        }
    }
    return true;
}

2.4.2 openFeign接口定义

操作模块:spzx-api-cart

1 RemoteCartService
@GetMapping("/deleteCartCheckedList/{userId}")
R<Boolean> deleteCartCheckedList(
    @PathVariable("userId") Long userId, 
    @RequestHeader(SecurityConstants.FROM_SOURCE)String source
);
2 RemoteCartFallbackFactory
@Override
public R<Boolean> deleteCartCheckedList(Long userId, String source) {
    return R.fail("删除用户购物车选中数据失败:" + throwable.getMessage());
}

2.5 获取用户地址信息(已定义)

2.5.1 远程调用接口

操作模块:spzx-user

1 UserAddressController
@InnerAuth
@GetMapping(value = "/getUserAddress/{id}")
public R<UserAddress> getUserAddress(@PathVariable("id") Long id)
{
    return R.ok(userAddressService.getById(id));
}
2 UserAddress

将spzx-user模块UserAddress实体类,移动到spzx-api-user模块

2.5.2 openFeign接口定义

操作模块:spzx-api-user

1 RemoteUserAddressService
package com.spzx.user.api;

@FeignClient(
    contextId = "remoteUserAddressService" , 
    value = ServiceNameConstants.USER_SERVICE, 
    fallbackFactory = RemoteUserAddressFallbackFactory.class
)
public interface RemoteUserAddressService {

    @GetMapping(value = "/userAddress/getUserAddress/{id}")
    R<UserAddress> getUserAddress(
            @PathVariable("id") Long id, 
            @RequestHeader(SecurityConstants.FROM_SOURCE) String source
    );
}
2 RemoteUserAddressFallbackFactory
package com.spzx.user.api.factory;

/**
 * 服务降级处理
 *
 */
@Component
@Slf4j
public class RemoteUserAddressFallbackFactory implements FallbackFactory<RemoteUserAddressService>
{
    @Override
    public RemoteUserAddressService create(Throwable throwable)
    {
        log.error("用户服务调用失败:{}", throwable.getMessage());
        return new RemoteUserAddressService()
        {

            @Override
            public R<UserAddress> getUserAddress(Long id, String source) {
                return R.fail("获取用户地址失败:" + throwable.getMessage());
            }
        };
    }
}
3 加载配置类

resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.spzx.user.api.factory.RemoteUserAddressFallbackFactory

2.6 批量查询商品实时价格

获取最新商品sku价格与购物车价格比较,校验价格是否变化,价格变化就更新购物车价格

2.6.1 批量查询商品实时价格

操作模块:spzx-product

1 ProductSkuController
@Operation(summary = "批量获取商品sku最新价格信息")
@InnerAuth
@PostMapping(value = "/getSkuPriceList")
public R<List<SkuPriceVo>> getSkuPriceList(@RequestBody List<Long> skuIdList)
{
    return R.ok(productSkuService.getSkuPriceList(skuIdList));
}
2 IProductService
List<SkuPriceVo> getSkuPriceList(List<Long> skuIdList);
3 ProductServiceImpl
@Override
public List<SkuPriceVo> getSkuPriceList(List<Long> skuIdList) {
    List<ProductSku> productSkuList = baseMapper.selectList(
        new LambdaQueryWrapper<ProductSku>()
        .in(ProductSku::getId, skuIdList)
        .select(ProductSku::getId, ProductSku::getSalePrice)
    );
    return productSkuList.stream().map(item -> {
        SkuPriceVo skuPrice = new SkuPriceVo();
        skuPrice.setSkuId(item.getId());
        skuPrice.setSalePrice(item.getSalePrice());
        return skuPrice;
    }).collect(Collectors.toList());
}

2.6.2 批量查询商品实时价格openFeign接口定义

操作模块:spzx-api-product

1 RemoteProductService
@PostMapping(value = "/productSku/getSkuPriceList")
R<List<SkuPriceVo>> getSkuPriceList(
    @RequestBody List<Long> skuIdList, 
    @RequestHeader(SecurityConstants.FROM_SOURCE) String source
);
2 RemoteProductFallbackFactory
@Override
public R<List<SkuPriceVo>> getSkuPriceList(List<Long> skuIdList, String source) {
    return R.fail("获取商品sku价格列表失败:" + throwable.getMessage());
}

2.6 后端业务接口

操作模块:spzx-order

2.6.1 OrderForm

package com.spzx.order.form;

@Data
public class OrderForm {

    @Schema(description = "用户流水号")
    private String tradeNo;

    //送货地址id
    @Schema(description = "送货地址id")
    private Long userAddressId;

    //运费
    @Schema(description = "运费")
    private BigDecimal feightFee;
    
    //备注
    @Schema(description = "备注")
    private String remark;
    
    @Schema(description = "结算商品列表")
    private List<OrderItem> orderItemList;
    
    @Schema(description = "是否是立即购买")
    private Boolean isBuy = false;
}

2.6.2 OrderInfoController

@Operation(summary = "用户提交订单")
@PostMapping("/submitOrder")
public AjaxResult submitOrder(@RequestBody OrderForm orderForm) {
    return success(orderInfoService.submitOrder(orderForm));
}

2.6.3 IOrderInfoService

Long submitOrder(OrderForm orderForm);

2.6.4 OrderInfoServiceImpl

@Autowired
private RemoteProductService remoteProductService;

@Autowired
private RemoteUserAddressService remoteUserAddressService;

@Autowired
private OrderLogMapper orderLogMapper;

/**
 * 验证页面提交流水号是否有效
 *
 * @param userId
 * @param tradeNo
 * @return
 */
private Boolean checkTradeNo(String userId, String tradeNo) {
    String userTradeKey = "user:tradeNo:" + userId;
    String redisTradeNo = (String) redisTemplate.opsForValue().get(userTradeKey);
    return tradeNo.equals(redisTradeNo);
}


/**
 * 删除流水号
 *
 * @param userId
 */
private void deleteTradeNo(String userId) {
    String userTradeKey = "user:tradeNo:" + userId;
    redisTemplate.delete(userTradeKey);
}

@Transactional(rollbackFor = Exception.class)
@Override
public Long submitOrder(OrderForm orderForm) {

    //获取用户的userId
    Long userId = SecurityContextHolder.getUserId();

    //1 防止订单重复提交
    //判断订单号是否存在:
    // 如果存在则说明用户第一次提交此订单
    // 如果五分中内提交了第二次订单,则订单号不存在,则说明用户已经提交过该订单,不能重复提交
    // 如果五分中之后第一次提交,则订单号也不存在,说明操作超时
    /*if(!this.checkTradeNo(userId.toString(), orderForm.getTradeNo())){
            throw new ServiceException("请勿重复提交订单");
        }
        //删除redis中的订单号
        this.deleteTradeNo(userId.toString());*/

    //使用lua解决原子性的问题
    String userTradeKey = "user:tradeNo:" + userId;
    // 将键的值更为 processed  表示已处理  return 1 表示处理成功
    // return -1  键存在但值不匹配,表示重复提交
    // return 0   键不存在,订单过期
    String scriptText = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            redis.call('set', KEYS[1], 'processed')
            return 1
        else
           if redis.call('exists', KEYS[1]) == 1 then
              redis.call('del',KEYS[1])
              return -1
           else
              return 0
           end
       end
       """;
    DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
    redisScript.setScriptText(scriptText);
    redisScript.setResultType(Long.class);
    Long flag = (Long)redisTemplate.execute(redisScript, Arrays.asList(userTradeKey), orderForm.getTradeNo());
    if (flag == 0) {
        throw new ServiceException("操作超时,请退回重试");
    }
    if(flag == -1){
        throw new ServiceException("请勿重复提交订单");
    }

    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    }

    //2 获取最新价格并判断价格是否改变
    //批量获取最新价格
    List<OrderItem> orderItemList = orderForm.getOrderItemList();
    List<Long> skuIdList = orderItemList.stream().map(orderItem -> orderItem.getSkuId()).collect(Collectors.toList());
    R<List<SkuPriceVo>> skuPriceListResult =remoteProductService.getSkuPriceList(skuIdList, SecurityConstants.INNER);
    if(R.FAIL == skuPriceListResult.getCode()){
        throw new ServiceException(skuPriceListResult.getMsg());
    }
    List<SkuPriceVo> skuPriceList = skuPriceListResult.getData();
    //判断价格是否发生变化
    String priceCheckResult = "";
    Map<Long, BigDecimal> skuIdSalePriceMap = skuPriceList.stream().collect(Collectors.toMap(SkuPriceVo::getSkuId, SkuPriceVo::getSalePrice));
    for (OrderItem orderItem : orderItemList) {
        if(orderItem.getSkuPrice().compareTo(skuIdSalePriceMap.get(orderItem.getSkuId())) != 0){
            //价格发生了变化
            priceCheckResult += orderItem.getSkuName() + "\n";
        }
    }
    if(!StringUtils.isEmpty(priceCheckResult)){

        if(!orderForm.getIsBuy()){
            //更新购物车中的价格
            remoteCartService.updateCartPrice(userId, SecurityConstants.INNER);
        }
        throw new ServiceException(priceCheckResult + "以上商品价格发生变化,请确认");
    }

    //3 校验库存并锁定库存 TODO

    //4 下单
    Long orderId = this.saveOrder(orderForm);

    //5 删除购物车选中商品
    if(!orderForm.getIsBuy()){
        remoteCartService.deleteCartCheckedList(userId, SecurityConstants.INNER);
    }
    return orderId;
}

private Long saveOrder(OrderForm orderForm) {

    Long userId = SecurityContextHolder.getUserId();
    String userName = SecurityContextHolder.getUserName();

    // 保存订单记录:order_info
    OrderInfo orderInfo = new OrderInfo();
    orderInfo.setUserId(userId);
    orderInfo.setOrderNo(orderForm.getTradeNo());//订单号
    orderInfo.setNickName(userName);
    orderInfo.setRemark(orderForm.getRemark());
    orderInfo.setFeightFee(orderForm.getFeightFee());

    //远程调用获取用户地址信息
    R<UserAddress> userAddressResult = remoteUserAddressService.getUserAddress(orderForm.getUserAddressId(), SecurityConstants.INNER);
    if(R.FAIL == userAddressResult.getCode()){
        throw new ServiceException(userAddressResult.getMsg());
    }
    //获取用户地址信息
    UserAddress userAddress = userAddressResult.getData();
    //保存用户地址信息
    orderInfo.setReceiverName(userAddress.getName());
    orderInfo.setReceiverPhone(userAddress.getPhone());
    orderInfo.setReceiverTagName(userAddress.getTagName());
    orderInfo.setReceiverProvince(userAddress.getProvinceCode());
    orderInfo.setReceiverCity(userAddress.getCityCode());
    orderInfo.setReceiverDistrict(userAddress.getDistrictCode());
    orderInfo.setReceiverAddress(userAddress.getFullAddress());

    //实时获取总价格
    //计算orderItem中商品的总价格
    List<OrderItem> orderItemList = orderForm.getOrderItemList();
    BigDecimal totalAmount = orderItemList
        .stream()
        .map(orderItem -> orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum())))
        .reduce(BigDecimal.ZERO, BigDecimal::add);
    //设置商品总价格
    orderInfo.setTotalAmount(totalAmount);
    orderInfo.setCouponAmount(new BigDecimal(0));//优惠券价格
    orderInfo.setOriginalTotalAmount(totalAmount);//原价
    //订单状态
    orderInfo.setOrderStatus(0);//待付款

    orderInfo.setCreateBy(userName);
    orderInfo.setUpdateBy(userName);
    baseMapper.insert(orderInfo);

    // 保存订单项记录:order_item
    for (OrderItem orderItem : orderItemList) {
        orderItem.setOrderId(orderInfo.getId());
        orderItem.setCreateBy(userName);
        orderItem.setUpdateBy(userName);
        orderItemMapper.insert(orderItem);
    }

    // 保存订单日志记录:order_log
    OrderLog orderLog = new OrderLog();
    orderLog.setOrderId(orderInfo.getId());
    orderLog.setProcessStatus(0);
    orderLog.setOperateUser(userName);
    orderLog.setNote("提交订单");
    orderLog.setCreateBy(userName);
    orderLog.setUpdateBy(userName);
    orderLogMapper.insert(orderLog);

    return orderInfo.getId();
}

3 立即购买

3.1、需求说明

入口:商品详情页,点击“立即购买”按钮,立即购买直接进入结算页,不经过购物车,结算页返回数据与正常下单结算数据一致,提交订单接口不变,如图所示:

buy

查看接口文档:

立即购买接口地址及返回结果:

get /order/orderInfo/buy/{skuId}
返回结果:
{
    "msg": "操作成功",
    "code": 200,
    "data": {
        "totalAmount": 5999.00,
        "orderItemList": [
            {
                "orderId": null,
                "skuId": 9,
                "skuName": "华为笔记本 32G",
                "thumbImg": "http://139.198.127.41:9000/spzx/20230525/c8f2eae0d36b6270.jpg.avif",
                "skuPrice": 5999.00,
                "skuNum": 1
            }
        ],
        "tradeNo": "1d76f36b59414e869e843fc742e21469"
    }
}

3.2、后端业务接口

操作模块:spzx-order

3.2.1、OrderInfoController

@Operation(summary = "立即购买")
@GetMapping("/buy/{skuId}")
public AjaxResult buy(@PathVariable Long skuId) {
    return success(orderInfoService.buy(skuId));
}

3.2.2、IOrderInfoService

TradeVo buy(Long skuId);

3.2.3、OrderInfoServiceImpl

@Override
public TradeVo buy(Long skuId) {

    //获取商品SKU信息
    R<ProductSku> productSkuResult = remoteProductService.getProductSku(skuId, SecurityConstants.INNER);
    if(R.FAIL == productSkuResult.getCode()){
        throw new ServiceException(productSkuResult.getMsg());
    }
    //获取实时价格
    R<SkuPriceVo> skuPriceResult = remoteProductService.getSkuPrice(skuId, SecurityConstants.INNER);
    if(R.FAIL == skuPriceResult.getCode()){
        throw new ServiceException(skuPriceResult.getMsg());
    }

    ProductSku productSku = productSkuResult.getData();
    SkuPriceVo skuPrice = skuPriceResult.getData();

    List<OrderItem> orderItemList = new ArrayList<>();
    OrderItem orderItem = new OrderItem();
    orderItem.setSkuId(skuId);
    orderItem.setSkuName(productSku.getSkuName());
    orderItem.setSkuNum(1);
    orderItem.setSkuPrice(skuPrice.getSalePrice());//填充最新价格
    orderItem.setThumbImg(productSku.getThumbImg());

    orderItemList.add(orderItem);


    //订单总金额
    BigDecimal totalAmount = skuPrice.getSalePrice();//填充最新价格

    //渲染订单确认页面-生成用户流水号
    String tradeNo = this.generateTradeNo(SecurityUtils.getUserId());

    TradeVo tradeVo = new TradeVo();
    tradeVo.setTotalAmount(totalAmount);
    tradeVo.setOrderItemList(orderItemList);
    tradeVo.setTradeNo(tradeNo);
    tradeVo.setIsBuy(true); //立即购买
    return tradeVo;
}

4 支付页

4.1 需求说明

提交订单成功,跳转到支付页面,根据订单id获取订单详细信息,展示订单支付信息

查看接口文档:

根据订单id获取订单信息接口地址及返回结果:

get /order/orderInfo/getOrderInfo/{orderId}
返回结果:
{
    "msg": "操作成功",
    "code": 200,
    "data": {
        "id": 2,
        "createTime": "2024-02-28 08:29:36",
        "userId": 1,
        "nickName": "13700032456",
        "orderNo": "f1866bad38bc4627958542d72a15ca9c",
        "couponId": null,
        "totalAmount": 9997.00,
        "couponAmount": 0.00,
        "originalTotalAmount": 9997.00,
        "feightFee": 0.00,
        "orderStatus": 0,
        "receiverName": "晴天",
        "receiverPhone": "15023656352",
        "receiverTagName": "家",
        "receiverProvince": "110000",
        "receiverCity": "110100",
        "receiverDistrict": "110101",
        "receiverAddress": "北京市北京市东城区东直门1号",
        "paymentTime": null,
        "deliveryTime": null,
        "receiveTime": null,
        "cancelTime": null,
        "cancelReason": null,
        "orderItemList": null
    }
}

4.2 后端业务接口

4.2.1 OrderInfoController

@Operation(summary = "获取订单信息")
@GetMapping("/getOrderInfo/{orderId}")
public AjaxResult getOrderInfo(@PathVariable Long orderId) {
    OrderInfo orderInfo = orderInfoService.getById(orderId);
    return success(orderInfo);
}

5 我的订单

5.1 需求说明

我的订单根据订单状态展示列表,如图所示:

order

查看接口文档:

我的订单接口地址及返回结果:

get /order/orderInfo/userOrderInfoList/{pageNum}/{pageSize}?orderStatus={orderStatus}
返回结果:
{
    "total": 2,
    "rows": [
        {
            "id": 2,
            "createTime": "2024-02-28 08:29:36",
            "userId": 1,
            "nickName": "13700032456",
            "orderNo": "f1866bad38bc4627958542d72a15ca9c",
            "couponId": null,
            "totalAmount": 9997.00,
            "couponAmount": 0.00,
            "originalTotalAmount": 9997.00,
            "feightFee": 0.00,
            "orderStatus": 0,
            "receiverName": "晴天",
            "receiverPhone": "15023656352",
            "receiverTagName": "家",
            "receiverProvince": "110000",
            "receiverCity": "110100",
            "receiverDistrict": "110101",
            "receiverAddress": "北京市北京市东城区东直门1号",
            "paymentTime": null,
            "deliveryTime": null,
            "receiveTime": null,
            "cancelTime": null,
            "cancelReason": null,
            "orderItemList": [
                {
                    "id": 4,
                    "orderId": 2,
                    "skuId": 7,
                    "skuName": "华为笔记本 8G",
                    "thumbImg": "http://139.198.127.41:9000/spzx/20230525/4b5a68a9bfbd0795.jpg.avif",
                    "skuPrice": 3999.00,
                    "skuNum": 2
                },
                ...
            ]
        },
        ...
    ],
    "code": 200,
    "msg": "查询成功"
}

5.2 后端业务接口

5.2.1 OrderInfoController

@Operation(summary = "获取用户订单分页列表")
@GetMapping("/userOrderInfoList/{pageNum}/{pageSize}")
public TableDataInfo list(
        @Parameter(name = "pageNum", description = "当前页码", required = true)
        @PathVariable Integer pageNum,

        @Parameter(name = "pageSize", description = "每页记录数", required = true)
        @PathVariable Integer pageSize,

        @Parameter(name = "orderStatus", description = "订单状态", required = false)
        @RequestParam(required = false, defaultValue = "") Integer orderStatus) {
    PageHelper.startPage(pageNum, pageSize);
    List<OrderInfo> list = orderInfoService.selectUserOrderInfoList(orderStatus);
    return getDataTable(list);
}

5.2.2 IOrderInfoService

List<OrderInfo> selectUserOrderInfoList(Integer orderStatus);

5.2.3 OrderInfoServiceI

@Override
public List<OrderInfo> selectUserOrderInfoList(Integer orderStatus) {

    // 获取当前登录用户的id
    Long userId = SecurityContextHolder.getUserId();

    List<OrderInfo> orderInfoList = baseMapper.selectList(new LambdaQueryWrapper<OrderInfo>()
                .eq(OrderInfo::getUserId, userId)
                .eq(orderStatus != null, OrderInfo::getOrderStatus, orderStatus)
                .orderByDesc(OrderInfo::getCreateTime));

    if(!CollectionUtils.isEmpty(orderInfoList)) {
        List<Long> orderIdList = orderInfoList.stream().map(OrderInfo::getId).collect(Collectors.toList());

        //查询orderItem
        List<OrderItem> orderItemList = orderItemMapper.selectList(
            new LambdaQueryWrapper<OrderItem>().in(OrderItem::getOrderId, orderIdList)
        );
        Map<Long, List<OrderItem>> orderIdToOrderItemListMap = orderItemList.stream().collect(
            Collectors.groupingBy(OrderItem::getOrderId)
        );

        //组装orderItemList
        orderInfoList.forEach(item -> {
            item.setOrderItemList(orderIdToOrderItemListMap.get(item.getId()));
        });
    }
    return orderInfoList;
}

6 订单详情

6.1 需求说明

我的订单点击详情,如图所示:

orderInfo

查看接口文档:

我的订单接口地址及返回结果:

get /order/orderInfo/getOrderInfo/{orderId}
返回结果:
{
    "msg": "操作成功",
    "code": 200,
    "data": {
        "id": 2,
        "createTime": "2024-02-28 08:29:36",
        "userId": 1,
        "nickName": "13700032456",
        "orderNo": "f1866bad38bc4627958542d72a15ca9c",
        "couponId": null,
        "totalAmount": 9997.00,
        "couponAmount": 0.00,
        "originalTotalAmount": 9997.00,
        "feightFee": 0.00,
        "orderStatus": 0,
        "receiverName": "晴天",
        "receiverPhone": "15023656352",
        "receiverTagName": "家",
        "receiverProvince": "110000",
        "receiverCity": "110100",
        "receiverDistrict": "110101",
        "receiverAddress": "北京市北京市东城区东直门1号",
        "paymentTime": null,
        "deliveryTime": null,
        "receiveTime": null,
        "cancelTime": null,
        "cancelReason": null,
        "orderItemList": [
            {
                "id": 4,
                "orderId": 2,
                "skuId": 7,
                "skuName": "华为笔记本 8G",
                "thumbImg": "http://139.198.127.41:9000/spzx/20230525/4b5a68a9bfbd0795.jpg.avif",
                "skuPrice": 3999.00,
                "skuNum": 2
            },
            ...
        ]
    }
}

6.2 后端业务接口

6.2.1 OrderInfoController

调整获取订单信息接口即可

@Operation(summary = "获取订单信息")
@GetMapping("/getOrderInfo/{orderId}")
public AjaxResult getOrderInfo(@PathVariable Long orderId) {
    OrderInfo orderInfo = orderInfoService.selectOrderInfoById(orderId);
    return success(orderInfo);
}

7 用户取消订单

7.1 需求说明

点击取消订单按钮可以取消订单。

1709899135809

操作模块:spzx-order

7.2 OrderInfoController

@Operation(summary = "取消订单")
@RequiresLogin
@GetMapping("/cancelOrder/{orderId}")
public AjaxResult cancelOrder(@PathVariable Long orderId) {
    orderInfoService.cancelOrder(orderId);
    return success();
}

7.3 IOrderInfoService

void cancelOrder(Long orderId);

7.4 OrderInfoServiceImpl

业务逻辑和延迟关单一致。

@Override
public void cancelOrder(Long orderId) {
    OrderInfo orderInfo = baseMapper.selectById(orderId);
    if(orderInfo != null && orderInfo.getOrderStatus().intValue() == 0) {//待付款
        orderInfo.setOrderStatus(-1);//已取消
        orderInfo.setCancelTime(new Date());
        orderInfo.setCancelReason("用户取消订单");
        baseMapper.updateById(orderInfo);
        //记录日志
        OrderLog orderLog = new OrderLog();
        orderLog.setOrderId(orderInfo.getId());
        orderLog.setProcessStatus(-1);
        orderLog.setNote("用户取消订单");
        orderLogMapper.insert(orderLog);
        //发送MQ消息通知商品系统解锁库存:TODO
        //rabbitService.sendMessage(MqConst.EXCHANGE_PRODUCT, MqConst.ROUTING_UNLOCK, orderInfo.getOrderNo());
    }
}