[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 orderTradeData() {
return success(orderInfoService.orderTradeData());
}
}
TradeVo orderTradeData();
@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;
}
需求说明:用户在结算页面点击提交订单按钮,那么此时就需要保存订单信息(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
}
操作模块: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/{userId}")
public R<Boolean> updateCartPrice(@PathVariable("userId") Long userId){
return R.ok(cartService.updateCartPrice(userId));
}
Boolean updateCartPrice(Long userId);
@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;
}
操作模块:spzx-api-cart
@GetMapping("/updateCartPrice/{userId}")
R<Boolean> updateCartPrice(
@PathVariable("userId") Long userId,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source
);
@Override
public R<Boolean> updateCartPrice(Long userId, String source) {
return R.fail("更新购物车价格失败:" + throwable.getMessage());
}
下单成功后,删除购物车选中的商品
操作模块:spzx-cart
@Operation(summary="删除用户购物车列表中选中商品列表")
@InnerAuth
@GetMapping("/deleteCartCheckedList/{userId}")
public R<Boolean> deleteCartCheckedList(@PathVariable("userId") Long userId){
return R.ok(cartService.deleteCartCheckedList(userId));
}
Boolean deleteCartCheckedList(Long userId);
@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;
}
操作模块:spzx-api-cart
@GetMapping("/deleteCartCheckedList/{userId}")
R<Boolean> deleteCartCheckedList(
@PathVariable("userId") Long userId,
@RequestHeader(SecurityConstants.FROM_SOURCE)String source
);
@Override
public R<Boolean> deleteCartCheckedList(Long userId, 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
获取最新商品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-api-product
@PostMapping(value = "/productSku/getSkuPriceList")
R<List<SkuPriceVo>> getSkuPriceList(
@RequestBody List<Long> skuIdList,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source
);
@Override
public R<List<SkuPriceVo>> getSkuPriceList(List<Long> skuIdList, String source) {
return R.fail("获取商品sku价格列表失败:" + throwable.getMessage());
}
操作模块: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 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();
}
入口:商品详情页,点击“立即购买”按钮,立即购买直接进入结算页,不经过购物车,结算页返回数据与正常下单结算数据一致,提交订单接口不变,如图所示:
查看接口文档:
立即购买接口地址及返回结果:
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);
@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;
}
提交订单成功,跳转到支付页面,根据订单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());
}
}