[TOC]
[TOC]
[TOC]
入口:购物车点击去结算按钮 ,进入结算页面(订单确认页面),如图所示:
分析页面需要的数据:
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"
}
}
操作模块:spzx-user
添加如下方法:
@Operation(summary = "查询地区信息树形列表")
@GetMapping(value = "/treeSelect/{parentCode}")
public AjaxResult treeSelect(@PathVariable String parentCode) {
return success(regionService.treeSelect(parentCode));
}
List<Region> treeSelect(String parentCode);
@Override
public List<Region> treeSelect(String parentCode) {
return regionMapper.selectList(new LambdaQueryWrapper<Region>().eq(Region::getParentCode, parentCode));
}
在spzx-modules模块下新建子模块spzx-order
<?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>
在resources目录下新建banner.txt
Spring Boot Version: ${spring-boot.version}
Spring Application Name: ${spring.application.name}
_ _
(_) | |
_ __ _ _ ___ _ _ _ ______ ___ _ _ ___ | |_ ___ _ __ ___
| '__|| | | | / _ \ | | | || ||______|/ __|| | | |/ __|| __| / _ \| '_ ` _ \
| | | |_| || (_) || |_| || | \__ \| |_| |\__ \| |_ | __/| | | | | |
|_| \__,_| \___/ \__, ||_| |___/ \__, ||___/ \__| \___||_| |_| |_|
__/ | __/ |
|___/ |___/
在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}
在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
在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>
添加启动类
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" +
" ''-' `'-' `-..-' ");
}
}
在spzx-gateway-dev.yml配置文件中添加会员服务的网关信息
# 订单服务
- id: spzx-order
uri: lb://spzx-order
predicates:
- Path=/order/**
filters:
- StripPrefix=1
从其他模块中f复制GenMP,修改代码生成器中关于服务名的部分,配置需要生成的表
补充属性
@TableField(exist = false)
private List<OrderItem> orderItemList;
将orderStatus
的数据类型改成Integer
操作模块:spzx-cart
@Operation(summary = "查询用户购物车列表中选中商品列表")
@InnerAuth
@GetMapping("/getCartCheckedList")
public R<List<CartInfo>> getCartCheckedList() {
return R.ok(cartService.getCartCheckedList());
}
List<CartInfo> getCartCheckedList();
@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;
}
操作模块:spzx-api-cart
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);
}
/**
* 购物车服务的serviceid
*/
public static final String CART_SERVICE = "spzx-cart";
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());
}
};
}
}
resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.spzx.cart.api.factory.RemoteCartFallbackFactory
操作模块:spzx-order
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;
}
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 trade() {
return success(orderInfoService.orderTradeData());
}
}
TradeVo trade();
@Autowired
private RemoteCartService remoteCartService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 订单结算页相关参数封装
*
* @return
*/
@Override
public TradeVo trade() {
//0.创建订单结算VO对象
TradeVo tradeVo = new TradeVo();
//1.远程调用购物车服务,获取选中的商品列表,封装订单明细列表
R<List<CartInfo>> r = cartService.getCartCheckedList(SecurityConstants.INNER);
//1.1 验证远程调用是否成功
if (R.FAIL == r.getCode()) {
throw new ServiceException("远程调用购物车服务失败,原因:" + r.getMsg());
}
//1.2 获取到选中购物车商品列表
List<CartInfo> cartInfoList = r.getData();
//1.3 将购物车商品列表转为订单明细列表
if (Collections.isEmpty(cartInfoList)) {
throw new ServiceException("没有需要结算商品");
}
//1.4 将购物车商品转为订单明细
List<OrderItem> orderItemList = cartInfoList
.stream()
.map(cartInfo -> {
OrderItem orderItem = new OrderItem();
BeanUtils.copyProperties(cartInfo, orderItem);
return orderItem;
}).collect(Collectors.toList());
tradeVo.setOrderItemList(orderItemList);
//2.计算订单总金额
BigDecimal totalAmount = new BigDecimal(0);
for (OrderItem orderItem : orderItemList) {
totalAmount = totalAmount.add(orderItem.getSkuPrice().multiply(new BigDecimal(orderItem.getSkuNum())));
}
tradeVo.setTotalAmount(totalAmount);
//3.生成本次订单流水号-防止重复点击提交订单,存入Redis中 有效期5分钟
String tradeNo = this.generateTradeNo();
tradeVo.setTradeNo(tradeNo);
//4.封装结算对象VO响应
return tradeVo;
}
/**
* 生成当前用户结算订单流水号
*
* @return
*/
@Override
public String generateTradeNo() {
String tradeKey = "user:tradeNo:" + SecurityContextHolder.getUserId();
String tradeNo = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(tradeKey, tradeNo, 5, TimeUnit.MINUTES);
return tradeNo;
}
需求说明:用户在结算页面点击提交订单按钮,那么此时就需要保存订单信息(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
}
获取最新商品sku价格与购物车价格比较,校验价格是否变化,价格变化就更新购物车价格
操作模块:spzx-product
@Operation(summary = "批量获取商品sku最新价格信息")
@InnerAuth
@PostMapping(value = "/getSkuPriceList")
public R<List<SkuPriceVo>> getSkuPriceList(@RequestBody List<Long> skuIdList)
{
return R.ok(productSkuService.getSkuPriceList(skuIdList));
}
List<SkuPriceVo> getSkuPriceList(List<Long> skuIdList);
@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());
}
操作模块:spzx-product
@Operation(summary = "获取商品sku最新价格信息")
@InnerAuth
@GetMapping(value = "/getSkuPrice/{skuId}")
public R<SkuPriceVo> getSkuPrice(@PathVariable("skuId") Long skuId)
{
return R.ok(productSkuService.getSkuPrice(skuId));
}
操作模块:spzx-api-product
将spzx-product模块SkuPriceVo类移动到spzx-api-product模块
@GetMapping(value = "/productSku/getSkuPrice/{skuId}")
R<SkuPriceVo> getSkuPrice(@PathVariable("skuId") Long skuId, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);
@Override
public R<SkuPriceVo> getSkuPrice(Long skuId, String source) {
return R.fail("获取商品sku价格失败:" + throwable.getMessage());
}
操作模块:spzx-cart
@Operation(summary="更新用户购物车列表价格")
@InnerAuth
@GetMapping("/updateCartPrice")
public R<Boolean> updateCartPrice(){
return R.ok(cartService.updateCartPrice());
}
Boolean updateCartPrice();
@Override
public Boolean updateCartPrice() {
String cartKey = getCartKey(SecurityContextHolder.getUserId());
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;
}
操作模块:spzx-api-cart
@GetMapping("/updateCartPrice/{userId}")
R<Boolean> updateCartPrice(
@RequestHeader(SecurityConstants.FROM_SOURCE) String source
);
@Override
public R<Boolean> updateCartPrice(String source) {
return R.fail("更新购物车价格失败:" + throwable.getMessage());
}
下单成功后,删除购物车选中的商品
操作模块:spzx-cart
@Operation(summary="删除用户购物车列表中选中商品列表")
@InnerAuth
@GetMapping("/deleteCartCheckedList")
public R<Boolean> deleteCartCheckedList(){
return R.ok(cartService.deleteCartCheckedList());
}
Boolean deleteCartCheckedList();
@Override
public Boolean deleteCartCheckedList() {
String cartKey = getCartKey(SecurityContextHolder.getUserId());
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;
}
操作模块:spzx-api-cart
@GetMapping("/deleteCartCheckedList/{userId}")
R<Boolean> deleteCartCheckedList(
@RequestHeader(SecurityConstants.FROM_SOURCE)String source
);
@Override
public R<Boolean> deleteCartCheckedList(String source) {
return R.fail("删除用户购物车选中数据失败:" + throwable.getMessage());
}
操作模块:spzx-user
@InnerAuth
@GetMapping(value = "/getUserAddress/{id}")
public R<UserAddress> getUserAddress(@PathVariable("id") Long id)
{
return R.ok(userAddressService.getById(id));
}
将spzx-user模块UserAddress实体类,移动到spzx-api-user模块
操作模块:spzx-api-user
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
);
}
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());
}
};
}
}
resources/META-INF.spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.spzx.user.api.factory.RemoteUserAddressFallbackFactory
操作模块:spzx-order
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;
}
@Operation(summary = "用户提交订单")
@PostMapping("/submitOrder")
public AjaxResult submitOrder(@RequestBody OrderForm orderForm) {
return success(orderInfoService.submitOrder(orderForm));
}
Long submitOrder(OrderForm orderForm);
@Autowired
private RemoteProductService remoteProductService;
@Autowired
private RemoteUserAddressService remoteUserAddressService;
@Autowired
private OrderLogMapper orderLogMapper;
/**
* 保存订单
*
* @param orderForm
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Long submitOrder(OrderForm orderForm) {
//1.业务校验,验证流水号,防止订单重复提交或长时间停留订单结算页
String tradeKey = "user:tradeNo:" + SecurityContextHolder.getUserId();
//1.1 通过Lua脚本验证流水号 KEYS[1]:流水号Key ARGV[1]:用户提交流水号
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end\n";
RedisScript<Boolean> booleanRedisScript = new DefaultRedisScript<>(script, Boolean.class);
Boolean flag = (Boolean) redisTemplate.execute(booleanRedisScript, Arrays.asList(tradeKey), orderForm.getTradeNo());
if (!flag) {
throw new ServiceException("流水号校验失败,请重新提交");
}
//2.验证订单中明细商品价格是否变更
//2.1 根据订单明细列表中skuID列表,远程调用"商品服务"得到实时商品价格列表
List<Long> skuIdList = orderForm.getOrderItemList().stream()
.map(OrderItem::getSkuId)
.collect(Collectors.toList());
R<List<SkuPriceVo>> r = remoteProductService.getSkuPriceList(skuIdList, SecurityConstants.INNER);
if (R.FAIL == r.getCode()) {
throw new ServiceException("远程调用商品服务失败,原因:" + r.getMsg());
}
List<SkuPriceVo> skuPriceVoList = r.getData();
Map<Long, BigDecimal> skuPriceMap =
skuPriceVoList.stream().collect(Collectors.toMap(SkuPriceVo::getSkuId, SkuPriceVo::getSalePrice));
//2.2 判断商品价格是否有变更,如果有变更
String priceErrorMsg = "";
for (OrderItem orderItem : orderForm.getOrderItemList()) {
if (orderItem.getSkuPrice().compareTo(skuPriceMap.get(orderItem.getSkuId())) != 0) {
priceErrorMsg += orderItem.getSkuName() + "\n";
}
}
if (StringUtils.isNotBlank(priceErrorMsg)) {
//2.2.1 如果是通过购物车车途径提交订单,则远程调用"购物车服务"更新购物车中商品价格
if (!orderForm.getIsBuy()) {
remoteCartService.updateCartPrice(SecurityConstants.INNER);
}
//2.2.2 则提示价格有变更,重新结算,引导用户购物车页面
throw new ServiceException(priceErrorMsg + "以上商品价格有变动");
}
//4.如果商品价格未变更:保存订单及明细,日志
Long orderId = this.saveOrder(orderForm);
//5.TODO 锁定商品库存,基于RabbitMQ异步锁定库存
//6.保存订单成功后, 如果是通过购物车车途径提交订单,清理用户购物车
if (!orderForm.getIsBuy()) {
remoteCartService.deleteCartCheckedList(SecurityConstants.INNER);
}
//7.响应订单ID,对接支付页面
//TODO :RabbitMQ(死信、延迟插件)发送延迟消息 作用:自动将超时未支付订单关闭(延迟关单)
return orderId;
}
/**
* 保存订单相关信息
*
* @param orderForm
* @return
*/
@Override
public Long saveOrder(OrderForm orderForm) {
//1.保存订单信息
Long userId = SecurityContextHolder.getUserId();
String userName = SecurityContextHolder.getUserName();
OrderInfo orderInfo = new OrderInfo();
//1.1 用户信息
orderInfo.setUserId(userId);
orderInfo.setNickName(userName);
//1.2 生成订单编号 形式:日期+全局唯一ID(雪花算法)
String orderNo = DateUtils.dateTime() + IdWorker.getIdStr();
orderInfo.setOrderNo(orderNo);
//1.3 遍历订单明细列表计算订单总金额
BigDecimal totalAmount = orderForm.getOrderItemList().stream()
.map(OrderItem::getSkuPrice)
//从0开始累加结合中每个元素值
.reduce(BigDecimal.ZERO, BigDecimal::add);
orderInfo.setTotalAmount(totalAmount);
orderInfo.setOriginalTotalAmount(totalAmount);
//1.4 订单状态:待付款
orderInfo.setOrderStatus(0);
orderInfo.setCreateBy(userName);
orderInfo.setRemark(orderForm.getRemark());
//1.5 封装订单收件人信息 远程调用"用户服务"得到收件人信息
R<UserAddress> r = remoteUserAddressService.getUserAddress(orderForm.getUserAddressId(), SecurityConstants.INNER);
if (R.FAIL == r.getCode()) {
throw new ServiceException("远程调用用户服务失败,原因:" + r.getMsg());
}
UserAddress userAddress = r.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.getAddress());
save(orderInfo);
Long orderId = orderInfo.getId();
//2.保存订单明细信息
List<OrderItem> orderItemList = orderForm.getOrderItemList();
if (!CollectionUtils.isEmpty(orderItemList)) {
for (OrderItem orderItem : orderItemList) {
orderItem.setOrderId(orderId);
orderItem.setCreateBy(userName);
orderItemMapper.insert(orderItem);
}
}
//3.保存订单操作日志
OrderLog orderLog = new OrderLog();
orderLog.setOrderId(orderId);
orderLog.setOperateUser(userId.toString());
orderLog.setProcessStatus(0);
orderLog.setNote(orderForm.getRemark());
orderLog.setCreateBy(userName);
orderLogMapper.insert(orderLog);
return orderId;
}
入口:商品详情页,点击“立即购买”按钮,立即购买直接进入结算页,不经过购物车,结算页返回数据与正常下单结算数据一致,提交订单接口不变,如图所示:
查看接口文档:
立即购买接口地址及返回结果:
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"
}
}
操作模块:spzx-order
@Operation(summary = "立即购买")
@GetMapping("/buy/{skuId}")
public AjaxResult buy(@PathVariable Long skuId) {
return success(orderInfoService.buy(skuId));
}
TradeVo buy(Long skuId);
/**
* 订单结算页面渲染
*
* @param skuId
* @return
*/
@Override
public TradeVo buy(Long skuId) {
//1.创建订单确认VO对象
TradeVo tradeVo = new TradeVo();
//2.封装订单明细
//2.1 远程调用"商品服务"查询商品信息
R<ProductSku> r = remoteProductService.getProductSku(skuId, SecurityConstants.INNER);
if (R.FAIL == r.getCode()) {
throw new ServiceException("远程调用商品服务失败,原因:" + r.getMsg());
}
ProductSku productSku = r.getData();
//2.2 封装订单明细列表
OrderItem orderItem = new OrderItem();
orderItem.setSkuId(skuId);
orderItem.setSkuName(productSku.getSkuName());
orderItem.setThumbImg(productSku.getThumbImg());
orderItem.setSkuPrice(productSku.getSalePrice());
orderItem.setSkuNum(1);
List<OrderItem> orderItemList = Arrays.asList(orderItem);
tradeVo.setOrderItemList(orderItemList);
//3.封装订单总金额
tradeVo.setTotalAmount(orderItem.getSkuPrice());
//4.生成流水号
String tradeNo = this.generateTradeNo();
tradeVo.setTradeNo(tradeNo);
//5.设置立即购买为:true
tradeVo.setIsBuy(true);
return tradeVo;
}
提交订单成功,跳转到支付页面,根据订单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
}
}
@Operation(summary = "获取订单信息")
@GetMapping("/getOrderInfo/{orderId}")
public AjaxResult getOrderInfo(@PathVariable Long orderId) {
OrderInfo orderInfo = orderInfoService.getById(orderId);
return success(orderInfo);
}
我的订单根据订单状态展示列表,如图所示:
查看接口文档:
我的订单接口地址及返回结果:
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": "查询成功"
}
@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);
}
List<OrderInfo> selectUserOrderInfoList(Integer orderStatus);
@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;
}
我的订单
点击详情
,如图所示:
查看接口文档:
我的订单接口地址及返回结果:
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
},
...
]
}
}
调整获取订单信息
接口即可
@Operation(summary = "获取订单信息")
@GetMapping("/getOrderInfo/{orderId}")
public AjaxResult getOrderInfo(@PathVariable Long orderId) {
OrderInfo orderInfo = orderInfoService.selectOrderInfoById(orderId);
return success(orderInfo);
}
点击取消订单
按钮可以取消订单。
操作模块:spzx-order
@Operation(summary = "取消订单")
@RequiresLogin
@GetMapping("/cancelOrder/{orderId}")
public AjaxResult cancelOrder(@PathVariable Long orderId) {
orderInfoService.cancelOrder(orderId);
return success();
}
void cancelOrder(Long orderId);
业务逻辑和延迟关单一致。
@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());
}
}