feat:使用liteflow

This commit is contained in:
amos
2025-11-03 14:28:17 +08:00
parent a64d6b55b6
commit 59fbc53daa
20 changed files with 1200 additions and 11 deletions

View File

@@ -0,0 +1,45 @@
package pers.amos.mall.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 库存操作类型枚举
*/
@Getter
@AllArgsConstructor
public enum InventoryOperationTypeEnum {
/**
* 锁定
*/
LOCK("LOCK", "锁定"),
/**
* 解锁
*/
UNLOCK("UNLOCK", "解锁"),
/**
* 扣减
*/
DEDUCT("DEDUCT", "扣减"),
/**
* 释放
*/
RELEASE("RELEASE", "释放");
private final String code;
private final String desc;
public static InventoryOperationTypeEnum fromCode(String code) {
for (InventoryOperationTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
throw new IllegalArgumentException("未知的库存操作类型: " + code);
}
}

View File

@@ -0,0 +1,35 @@
package pers.amos.mall.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 运营模式枚举
*/
@Getter
@AllArgsConstructor
public enum OperationModeEnum {
/**
* 固定班次
*/
FIXED("FIXED", "固定班次"),
/**
* 滚动发车
*/
ROLLING("ROLLING", "滚动发车");
private final String code;
private final String desc;
public static OperationModeEnum fromCode(String code) {
for (OperationModeEnum mode : values()) {
if (mode.getCode().equals(code)) {
return mode;
}
}
throw new IllegalArgumentException("未知的运营模式: " + code);
}
}

View File

@@ -0,0 +1,55 @@
package pers.amos.mall.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单状态枚举
*/
@Getter
@AllArgsConstructor
public enum OrderStatusEnum {
/**
* 待支付
*/
UNPAID("UNPAID", "待支付"),
/**
* 已支付
*/
PAID("PAID", "已支付"),
/**
* 使用中
*/
USING("USING", "使用中"),
/**
* 已完成
*/
COMPLETED("COMPLETED", "已完成"),
/**
* 已取消
*/
CANCELLED("CANCELLED", "已取消"),
/**
* 已退款
*/
REFUNDED("REFUNDED", "已退款");
private final String code;
private final String desc;
public static OrderStatusEnum fromCode(String code) {
for (OrderStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("未知的订单状态: " + code);
}
}

View File

@@ -0,0 +1,35 @@
package pers.amos.mall.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单类型枚举
*/
@Getter
@AllArgsConstructor
public enum OrderTypeEnum {
/**
* 固定班次
*/
FIXED("FIXED", "固定班次"),
/**
* 滚动发车
*/
ROLLING("ROLLING", "滚动发车");
private final String code;
private final String desc;
public static OrderTypeEnum fromCode(String code) {
for (OrderTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
throw new IllegalArgumentException("未知的订单类型: " + code);
}
}

View File

@@ -0,0 +1,45 @@
package pers.amos.mall.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 车票状态枚举
*/
@Getter
@AllArgsConstructor
public enum TicketStatusEnum {
/**
* 有效
*/
VALID("VALID", "有效"),
/**
* 已使用
*/
USED("USED", "已使用"),
/**
* 已过期
*/
EXPIRED("EXPIRED", "已过期"),
/**
* 已退款
*/
REFUNDED("REFUNDED", "已退款");
private final String code;
private final String desc;
public static TicketStatusEnum fromCode(String code) {
for (TicketStatusEnum status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("未知的车票状态: " + code);
}
}

View File

@@ -0,0 +1,40 @@
package pers.amos.mall.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 票种类型枚举
*/
@Getter
@AllArgsConstructor
public enum TicketTypeEnum {
/**
* 成人票
*/
ADULT("ADULT", "成人票"),
/**
* 学生票
*/
STUDENT("STUDENT", "学生票"),
/**
* 儿童票
*/
CHILD("CHILD", "儿童票");
private final String code;
private final String desc;
public static TicketTypeEnum fromCode(String code) {
for (TicketTypeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
throw new IllegalArgumentException("未知的票种类型: " + code);
}
}

View File

@@ -0,0 +1,105 @@
package pers.amos.mall.perform.service;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import pers.amos.mall.api.dto.TicketDTO;
import pers.amos.mall.api.dto.TicketGenerateDTO;
import pers.amos.mall.api.service.TicketService;
import pers.amos.mall.common.enums.TicketStatusEnum;
import pers.amos.mall.common.response.Result;
import pers.amos.mall.perform.dal.dataobject.Ticket;
import pers.amos.mall.perform.dal.repository.TicketRepository;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 车票服务Dubbo实现
*/
@Slf4j
@DubboService
public class TicketServiceImpl implements TicketService {
@Autowired
private TicketRepository ticketRepository;
@Override
public Result<List<TicketDTO>> generateTickets(TicketGenerateDTO dto) {
try {
List<Ticket> tickets = new ArrayList<>();
for (int i = 0; i < dto.getQuantity(); i++) {
Ticket ticket = new Ticket();
ticket.setTicketNo(generateTicketNo());
ticket.setOrderNo(dto.getOrderNo());
ticket.setUserNo(dto.getUserNo());
ticket.setRouteCode(dto.getRouteCode());
ticket.setOrderType(dto.getOrderType());
ticket.setScheduleCode(dto.getScheduleCode());
ticket.setRollingScheduleCode(dto.getRollingScheduleCode());
ticket.setTicketType(dto.getTicketType());
// 生成二维码内容(这里简单使用票号,实际应该加密)
ticket.setQrCode(encryptTicketNo(ticket.getTicketNo()));
ticket.setStatus(TicketStatusEnum.VALID.getCode());
ticket.setIssueTime(LocalDateTime.now());
ticket.setCreateTime(LocalDateTime.now());
ticket.setUpdateTime(LocalDateTime.now());
tickets.add(ticket);
}
// 批量保存
ticketRepository.saveBatch(tickets);
log.info("车票生成成功, orderNo={}, quantity={}", dto.getOrderNo(), dto.getQuantity());
// 转换为DTO
List<TicketDTO> ticketDTOs = tickets.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return Result.success(ticketDTOs);
} catch (Exception e) {
log.error("车票生成失败, orderNo={}", dto.getOrderNo(), e);
return Result.fail("TICKET_GENERATE_ERROR", "车票生成失败:" + e.getMessage());
}
}
/**
* 生成车票号
* 格式TKT + yyyyMMddHHmmssSSS + 6位随机数
*/
private String generateTicketNo() {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
String random = RandomUtil.randomNumbers(6);
return "TKT" + timestamp + random;
}
/**
* 加密车票号生成二维码内容
* 这里简化处理实际应该使用AES等加密算法
*/
private String encryptTicketNo(String ticketNo) {
// TODO: 实际应该使用加密算法
return "QR_" + ticketNo;
}
/**
* 转换为DTO
*/
private TicketDTO convertToDTO(Ticket ticket) {
TicketDTO dto = new TicketDTO();
BeanUtils.copyProperties(ticket, dto);
return dto;
}
}

View File

@@ -11,7 +11,6 @@ spring:
username: root
password: root
# Jackson配置
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
@@ -45,13 +44,10 @@ dubbo:
registry:
address: nacos://localhost:8848
scan:
base-packages: pers.amos.mall.perform.api
base-packages: pers.amos.mall.perform.service
# RocketMQ配置
rocketmq:
name-server: localhost:9876
producer:
group: perform-producer-group
consumer:
group: perform-consumer-group

View File

@@ -0,0 +1,346 @@
package pers.amos.mall.route.service;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.beans.factory.annotation.Autowired;
import pers.amos.mall.api.dto.InventoryCheckDTO;
import pers.amos.mall.api.dto.InventoryOperationDTO;
import pers.amos.mall.api.service.RouteService;
import pers.amos.mall.common.enums.InventoryOperationTypeEnum;
import pers.amos.mall.common.enums.OrderTypeEnum;
import pers.amos.mall.common.response.Result;
import pers.amos.mall.route.dal.dataobject.FixedSchedule;
import pers.amos.mall.route.dal.dataobject.InventoryLog;
import pers.amos.mall.route.dal.dataobject.RollingSchedule;
import pers.amos.mall.route.dal.repository.FixedScheduleRepository;
import pers.amos.mall.route.dal.repository.InventoryLogRepository;
import pers.amos.mall.route.dal.repository.RollingScheduleRepository;
import java.time.LocalDateTime;
/**
* 线路服务Dubbo实现
*/
@Slf4j
@DubboService
public class RouteServiceImpl implements RouteService {
@Autowired
private FixedScheduleRepository fixedScheduleRepository;
@Autowired
private RollingScheduleRepository rollingScheduleRepository;
@Autowired
private InventoryLogRepository inventoryLogRepository;
@Override
public Result<Boolean> checkInventory(InventoryCheckDTO dto) {
try {
if (OrderTypeEnum.FIXED.getCode().equals(dto.getOrderType())) {
return checkFixedScheduleInventory(dto);
} else {
return checkRollingScheduleInventory(dto);
}
} catch (Exception e) {
log.error("检查库存失败", e);
return Result.fail("INVENTORY_CHECK_ERROR", "检查库存失败:" + e.getMessage());
}
}
@Override
public Result<Boolean> lockInventory(InventoryOperationDTO dto) {
try {
if (OrderTypeEnum.FIXED.getCode().equals(dto.getOrderType())) {
return lockFixedScheduleInventory(dto);
} else {
return lockRollingScheduleInventory(dto);
}
} catch (Exception e) {
log.error("锁定库存失败, orderNo={}", dto.getOrderNo(), e);
return Result.fail("INVENTORY_LOCK_ERROR", "锁定库存失败:" + e.getMessage());
}
}
@Override
public Result<Boolean> deductInventory(InventoryOperationDTO dto) {
try {
if (OrderTypeEnum.FIXED.getCode().equals(dto.getOrderType())) {
return deductFixedScheduleInventory(dto);
} else {
return deductRollingScheduleInventory(dto);
}
} catch (Exception e) {
log.error("扣减库存失败, orderNo={}", dto.getOrderNo(), e);
return Result.fail("INVENTORY_DEDUCT_ERROR", "扣减库存失败:" + e.getMessage());
}
}
@Override
public Result<Boolean> releaseInventory(InventoryOperationDTO dto) {
try {
if (OrderTypeEnum.FIXED.getCode().equals(dto.getOrderType())) {
return releaseFixedScheduleInventory(dto);
} else {
return releaseRollingScheduleInventory(dto);
}
} catch (Exception e) {
log.error("释放库存失败, orderNo={}", dto.getOrderNo(), e);
return Result.fail("INVENTORY_RELEASE_ERROR", "释放库存失败:" + e.getMessage());
}
}
// ==================== 固定班次库存操作 ====================
private Result<Boolean> checkFixedScheduleInventory(InventoryCheckDTO dto) {
FixedSchedule schedule = fixedScheduleRepository.getOne(
Wrappers.lambdaQuery(FixedSchedule.class)
.eq(FixedSchedule::getScheduleCode, dto.getScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "班次不存在");
}
if (schedule.getAvailableSeats() < dto.getQuantity()) {
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
}
return Result.success(true);
}
private Result<Boolean> lockFixedScheduleInventory(InventoryOperationDTO dto) {
FixedSchedule schedule = fixedScheduleRepository.getOne(
Wrappers.lambdaQuery(FixedSchedule.class)
.eq(FixedSchedule::getScheduleCode, dto.getScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "班次不存在");
}
Integer beforeQty = schedule.getAvailableSeats();
// 乐观锁扣减可用库存
boolean success = fixedScheduleRepository.update(
Wrappers.lambdaUpdate(FixedSchedule.class)
.eq(FixedSchedule::getScheduleCode, dto.getScheduleCode())
.eq(FixedSchedule::getVersion, schedule.getVersion())
.setSql("available_seats = available_seats - " + dto.getQuantity())
.set(FixedSchedule::getVersion, schedule.getVersion() + 1)
.ge(FixedSchedule::getAvailableSeats, dto.getQuantity())
);
if (!success) {
return Result.fail("INVENTORY_LOCK_FAILED", "库存锁定失败,可能库存不足或并发冲突");
}
// 记录库存日志
saveInventoryLog(OrderTypeEnum.FIXED.getCode(), dto.getScheduleCode(), null, InventoryOperationTypeEnum.LOCK.getCode(),
dto.getQuantity(), beforeQty, beforeQty - dto.getQuantity(),
dto.getOrderNo(), "锁定库存");
log.info("固定班次库存锁定成功, scheduleCode={}, quantity={}, orderNo={}",
dto.getScheduleCode(), dto.getQuantity(), dto.getOrderNo());
return Result.success(true);
}
private Result<Boolean> deductFixedScheduleInventory(InventoryOperationDTO dto) {
// 固定班次在锁定时已经扣减了available_seats这里只需要增加sold_seats
FixedSchedule schedule = fixedScheduleRepository.getOne(
Wrappers.lambdaQuery(FixedSchedule.class)
.eq(FixedSchedule::getScheduleCode, dto.getScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "班次不存在");
}
Integer beforeSold = schedule.getSoldSeats();
fixedScheduleRepository.update(
Wrappers.lambdaUpdate(FixedSchedule.class)
.eq(FixedSchedule::getScheduleCode, dto.getScheduleCode())
.setSql("sold_seats = sold_seats + " + dto.getQuantity())
);
// 记录库存日志
saveInventoryLog(OrderTypeEnum.FIXED.getCode(), dto.getScheduleCode(), null, InventoryOperationTypeEnum.DEDUCT.getCode(),
dto.getQuantity(), beforeSold, beforeSold + dto.getQuantity(),
dto.getOrderNo(), "扣减库存(支付成功)");
log.info("固定班次库存扣减成功, scheduleCode={}, quantity={}, orderNo={}",
dto.getScheduleCode(), dto.getQuantity(), dto.getOrderNo());
return Result.success(true);
}
private Result<Boolean> releaseFixedScheduleInventory(InventoryOperationDTO dto) {
FixedSchedule schedule = fixedScheduleRepository.getOne(
Wrappers.lambdaQuery(FixedSchedule.class)
.eq(FixedSchedule::getScheduleCode, dto.getScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "班次不存在");
}
Integer beforeQty = schedule.getAvailableSeats();
// 释放库存
fixedScheduleRepository.update(
Wrappers.lambdaUpdate(FixedSchedule.class)
.eq(FixedSchedule::getScheduleCode, dto.getScheduleCode())
.setSql("available_seats = available_seats + " + dto.getQuantity())
);
// 记录库存日志
saveInventoryLog(OrderTypeEnum.FIXED.getCode(), dto.getScheduleCode(), null, InventoryOperationTypeEnum.RELEASE.getCode(),
dto.getQuantity(), beforeQty, beforeQty + dto.getQuantity(),
dto.getOrderNo(), "释放库存(订单取消/超时)");
log.info("固定班次库存释放成功, scheduleCode={}, quantity={}, orderNo={}",
dto.getScheduleCode(), dto.getQuantity(), dto.getOrderNo());
return Result.success(true);
}
// ==================== 滚动班次库存操作 ====================
private Result<Boolean> checkRollingScheduleInventory(InventoryCheckDTO dto) {
RollingSchedule schedule = rollingScheduleRepository.getOne(
Wrappers.lambdaQuery(RollingSchedule.class)
.eq(RollingSchedule::getRollingScheduleCode, dto.getRollingScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "滚动班次不存在");
}
if (schedule.getAvailableSeats() < dto.getQuantity()) {
return Result.fail("INSUFFICIENT_INVENTORY", "库存不足");
}
return Result.success(true);
}
private Result<Boolean> lockRollingScheduleInventory(InventoryOperationDTO dto) {
RollingSchedule schedule = rollingScheduleRepository.getOne(
Wrappers.lambdaQuery(RollingSchedule.class)
.eq(RollingSchedule::getRollingScheduleCode, dto.getRollingScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "滚动班次不存在");
}
Integer beforeQty = schedule.getAvailableSeats();
// 乐观锁扣减共享库存池
boolean success = rollingScheduleRepository.update(
Wrappers.lambdaUpdate(RollingSchedule.class)
.eq(RollingSchedule::getRollingScheduleCode, dto.getRollingScheduleCode())
.eq(RollingSchedule::getVersion, schedule.getVersion())
.setSql("available_seats = available_seats - " + dto.getQuantity())
.set(RollingSchedule::getVersion, schedule.getVersion() + 1)
.ge(RollingSchedule::getAvailableSeats, dto.getQuantity())
);
if (!success) {
return Result.fail("INVENTORY_LOCK_FAILED", "库存锁定失败,可能库存不足或并发冲突");
}
// 记录库存日志
saveInventoryLog(OrderTypeEnum.ROLLING.getCode(), null, dto.getRollingScheduleCode(), InventoryOperationTypeEnum.LOCK.getCode(),
dto.getQuantity(), beforeQty, beforeQty - dto.getQuantity(),
dto.getOrderNo(), "锁定共享库存池");
log.info("滚动班次库存锁定成功, rollingScheduleCode={}, quantity={}, orderNo={}",
dto.getRollingScheduleCode(), dto.getQuantity(), dto.getOrderNo());
return Result.success(true);
}
private Result<Boolean> deductRollingScheduleInventory(InventoryOperationDTO dto) {
RollingSchedule schedule = rollingScheduleRepository.getOne(
Wrappers.lambdaQuery(RollingSchedule.class)
.eq(RollingSchedule::getRollingScheduleCode, dto.getRollingScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "滚动班次不存在");
}
Integer beforeSold = schedule.getSoldSeats();
rollingScheduleRepository.update(
Wrappers.lambdaUpdate(RollingSchedule.class)
.eq(RollingSchedule::getRollingScheduleCode, dto.getRollingScheduleCode())
.setSql("sold_seats = sold_seats + " + dto.getQuantity())
);
// 记录库存日志
saveInventoryLog(OrderTypeEnum.ROLLING.getCode(), null, dto.getRollingScheduleCode(), InventoryOperationTypeEnum.DEDUCT.getCode(),
dto.getQuantity(), beforeSold, beforeSold + dto.getQuantity(),
dto.getOrderNo(), "扣减库存(支付成功)");
log.info("滚动班次库存扣减成功, rollingScheduleCode={}, quantity={}, orderNo={}",
dto.getRollingScheduleCode(), dto.getQuantity(), dto.getOrderNo());
return Result.success(true);
}
private Result<Boolean> releaseRollingScheduleInventory(InventoryOperationDTO dto) {
RollingSchedule schedule = rollingScheduleRepository.getOne(
Wrappers.lambdaQuery(RollingSchedule.class)
.eq(RollingSchedule::getRollingScheduleCode, dto.getRollingScheduleCode())
);
if (schedule == null) {
return Result.fail("SCHEDULE_NOT_FOUND", "滚动班次不存在");
}
Integer beforeQty = schedule.getAvailableSeats();
// 释放库存
rollingScheduleRepository.update(
Wrappers.lambdaUpdate(RollingSchedule.class)
.eq(RollingSchedule::getRollingScheduleCode, dto.getRollingScheduleCode())
.setSql("available_seats = available_seats + " + dto.getQuantity())
);
// 记录库存日志
saveInventoryLog(OrderTypeEnum.ROLLING.getCode(), null, dto.getRollingScheduleCode(), InventoryOperationTypeEnum.RELEASE.getCode(),
dto.getQuantity(), beforeQty, beforeQty + dto.getQuantity(),
dto.getOrderNo(), "释放库存(订单取消/超时)");
log.info("滚动班次库存释放成功, rollingScheduleCode={}, quantity={}, orderNo={}",
dto.getRollingScheduleCode(), dto.getQuantity(), dto.getOrderNo());
return Result.success(true);
}
// ==================== 库存日志记录 ====================
private void saveInventoryLog(String inventoryType, String scheduleCode,
String rollingScheduleCode, String operationType,
Integer quantity, Integer beforeQty, Integer afterQty,
String orderNo, String remark) {
InventoryLog log = new InventoryLog();
log.setInventoryType(inventoryType);
log.setScheduleCode(scheduleCode);
log.setRollingScheduleCode(rollingScheduleCode);
log.setOperationType(operationType);
log.setQuantity(quantity);
log.setBeforeQty(beforeQty);
log.setAfterQty(afterQty);
log.setOrderNo(orderNo);
log.setRemark(remark);
log.setCreateTime(LocalDateTime.now());
inventoryLogRepository.save(log);
}
}

View File

@@ -45,7 +45,7 @@ dubbo:
registry:
address: nacos://localhost:8848
scan:
base-packages: pers.amos.mall.route.api
base-packages: pers.amos.mall.route.service
# RocketMQ配置
rocketmq:

View File

@@ -0,0 +1,58 @@
package pers.amos.mall.trade.controller;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.amos.mall.api.dto.OrderCreateDTO;
import pers.amos.mall.api.dto.OrderResultDTO;
import pers.amos.mall.common.response.Result;
import pers.amos.mall.trade.liteflow.context.OrderContext;
/**
* 订单Controller
*/
@Slf4j
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private FlowExecutor flowExecutor;
/**
* 创建订单
*/
@PostMapping("/create")
public Result<OrderResultDTO> createOrder(@RequestBody OrderCreateDTO dto) {
log.info("收到创建订单请求, userNo={}, routeCode={}, orderType={}, quantity={}",
dto.getUserNo(), dto.getRouteCode(), dto.getOrderType(), dto.getQuantity());
try {
// 创建上下文
OrderContext context = new OrderContext();
context.setOrderCreateDTO(dto);
// 执行流程
LiteflowResponse response = flowExecutor.execute2Resp("orderChain", null, context);
if (response.isSuccess()) {
OrderResultDTO resultDTO = context.getOrderResultDTO();
log.info("订单创建成功, orderNo={}", resultDTO.getOrderNo());
return Result.success(resultDTO);
} else {
log.error("订单创建失败: {}", response.getMessage());
return Result.fail("ORDER_CREATE_FAIL", response.getMessage());
}
} catch (Exception e) {
log.error("订单创建异常", e);
return Result.fail("ORDER_CREATE_ERROR", "订单创建失败:" + e.getMessage());
}
}
}

View File

@@ -0,0 +1,42 @@
package pers.amos.mall.trade.liteflow.context;
import lombok.Data;
import pers.amos.mall.api.dto.OrderCreateDTO;
import pers.amos.mall.api.dto.OrderResultDTO;
import pers.amos.mall.api.dto.TicketDTO;
import pers.amos.mall.trade.dal.dataobject.Order;
import java.util.List;
/**
* 下单流程上下文
*/
@Data
public class OrderContext {
/**
* 输入创建订单DTO
*/
private OrderCreateDTO orderCreateDTO;
/**
* 中间数据:订单实体
*/
private Order order;
/**
* 中间数据:车票列表
*/
private List<TicketDTO> tickets;
/**
* 输出订单结果DTO
*/
private OrderResultDTO orderResultDTO;
/**
* 错误信息
*/
private String errorMessage;
}

View File

@@ -0,0 +1,35 @@
package pers.amos.mall.trade.liteflow.node;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import pers.amos.mall.api.dto.OrderResultDTO;
import pers.amos.mall.trade.dal.dataobject.Order;
import pers.amos.mall.trade.liteflow.context.OrderContext;
/**
* 构建返回结果节点
*/
@Slf4j
@Component("buildResult")
public class BuildResultNode extends NodeComponent {
@Override
public void process() throws Exception {
OrderContext context = this.getContextBean(OrderContext.class);
Order order = context.getOrder();
log.info("开始构建返回结果, orderNo={}", order.getOrderNo());
// 构建返回DTO
OrderResultDTO resultDTO = new OrderResultDTO();
BeanUtils.copyProperties(order, resultDTO);
resultDTO.setTickets(context.getTickets());
context.setOrderResultDTO(resultDTO);
log.info("返回结果构建成功, orderNo={}", order.getOrderNo());
}
}

View File

@@ -0,0 +1,59 @@
package pers.amos.mall.trade.liteflow.node;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Component;
import pers.amos.mall.api.dto.InventoryCheckDTO;
import pers.amos.mall.api.dto.OrderCreateDTO;
import pers.amos.mall.api.service.RouteService;
import pers.amos.mall.common.enums.OrderTypeEnum;
import pers.amos.mall.common.response.Result;
import pers.amos.mall.trade.liteflow.context.OrderContext;
/**
* 检查库存节点
*/
@Slf4j
@Component("checkInventory")
public class CheckInventoryNode extends NodeComponent {
@DubboReference
private RouteService routeService;
@Override
public void process() throws Exception {
OrderContext context = this.getContextBean(OrderContext.class);
OrderCreateDTO dto = context.getOrderCreateDTO();
log.info("开始检查库存, orderType={}, quantity={}", dto.getOrderType(), dto.getQuantity());
// 构建库存检查DTO
InventoryCheckDTO checkDTO = new InventoryCheckDTO();
checkDTO.setOrderType(dto.getOrderType());
checkDTO.setScheduleCode(dto.getScheduleCode());
checkDTO.setRollingScheduleCode(generateRollingScheduleCode(dto));
checkDTO.setQuantity(dto.getQuantity());
// Dubbo调用Route服务检查库存
Result<Boolean> result = routeService.checkInventory(checkDTO);
if (!result.isSuccess() || !Boolean.TRUE.equals(result.getData())) {
throw new RuntimeException("库存不足:" + result.getErrorDesc());
}
log.info("库存检查通过, orderType={}, quantity={}", dto.getOrderType(), dto.getQuantity());
}
/**
* 生成滚动班次编号
* 格式:线路编码 + 日期 + RS
*/
private String generateRollingScheduleCode(OrderCreateDTO dto) {
if (OrderTypeEnum.ROLLING.getCode().equals(dto.getOrderType())) {
return dto.getRouteCode() + "-" + dto.getScheduleDate().replace("-", "") + "-RS";
}
return null;
}
}

View File

@@ -0,0 +1,72 @@
package pers.amos.mall.trade.liteflow.node;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import pers.amos.mall.api.dto.OrderCreateDTO;
import pers.amos.mall.common.enums.OrderStatusEnum;
import pers.amos.mall.common.enums.OrderTypeEnum;
import pers.amos.mall.trade.dal.dataobject.Order;
import pers.amos.mall.trade.dal.repository.OrderRepository;
import pers.amos.mall.trade.liteflow.context.OrderContext;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 创建订单节点
*/
@Slf4j
@Component("createOrder")
public class CreateOrderNode extends NodeComponent {
@Autowired
private OrderRepository orderRepository;
@Override
public void process() throws Exception {
OrderContext context = this.getContextBean(OrderContext.class);
OrderCreateDTO dto = context.getOrderCreateDTO();
// 从上下文获取订单号临时存储在errorMessage中
String orderNo = context.getErrorMessage();
log.info("开始创建订单, orderNo={}", orderNo);
Order order = new Order();
order.setOrderNo(orderNo);
order.setUserNo(dto.getUserNo());
order.setRouteCode(dto.getRouteCode());
order.setOrderType(dto.getOrderType());
order.setScheduleCode(dto.getScheduleCode());
// 如果是滚动发车,设置滚动班次编号
if (OrderTypeEnum.ROLLING.getCode().equals(dto.getOrderType())) {
String rollingScheduleCode = dto.getRouteCode() + "-" +
dto.getScheduleDate().replace("-", "") + "-RS";
order.setRollingScheduleCode(rollingScheduleCode);
}
order.setTicketType(dto.getTicketType());
order.setQuantity(dto.getQuantity());
// TODO: 根据线路和票种查询价格,这里暂时写死
BigDecimal unitPrice = new BigDecimal("50.00");
order.setUnitPrice(unitPrice);
order.setTotalAmount(unitPrice.multiply(new BigDecimal(dto.getQuantity())));
order.setOrderStatus(OrderStatusEnum.UNPAID.getCode());
order.setCreateTime(LocalDateTime.now());
order.setExpireTime(LocalDateTime.now().plusMinutes(15)); // 15分钟支付超时
order.setUpdateTime(LocalDateTime.now());
orderRepository.save(order);
// 将订单保存到上下文
context.setOrder(order);
log.info("订单创建成功, orderNo={}, totalAmount={}", orderNo, order.getTotalAmount());
}
}

View File

@@ -0,0 +1,58 @@
package pers.amos.mall.trade.liteflow.node;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Component;
import pers.amos.mall.api.dto.TicketDTO;
import pers.amos.mall.api.dto.TicketGenerateDTO;
import pers.amos.mall.api.service.TicketService;
import pers.amos.mall.common.response.Result;
import pers.amos.mall.trade.dal.dataobject.Order;
import pers.amos.mall.trade.liteflow.context.OrderContext;
import java.util.List;
/**
* 生成车票节点
*/
@Slf4j
@Component("generateTickets")
public class GenerateTicketsNode extends NodeComponent {
@DubboReference
private TicketService ticketService;
@Override
public void process() throws Exception {
OrderContext context = this.getContextBean(OrderContext.class);
Order order = context.getOrder();
log.info("开始生成车票, orderNo={}, quantity={}", order.getOrderNo(), order.getQuantity());
// 构建生成车票DTO
TicketGenerateDTO generateDTO = new TicketGenerateDTO();
generateDTO.setOrderNo(order.getOrderNo());
generateDTO.setUserNo(order.getUserNo());
generateDTO.setRouteCode(order.getRouteCode());
generateDTO.setOrderType(order.getOrderType());
generateDTO.setScheduleCode(order.getScheduleCode());
generateDTO.setRollingScheduleCode(order.getRollingScheduleCode());
generateDTO.setTicketType(order.getTicketType());
generateDTO.setQuantity(order.getQuantity());
// Dubbo调用Perform服务生成车票
Result<List<TicketDTO>> result = ticketService.generateTickets(generateDTO);
if (!result.isSuccess() || result.getData() == null) {
throw new RuntimeException("车票生成失败:" + result.getErrorDesc());
}
// 将车票保存到上下文
context.setTickets(result.getData());
log.info("车票生成成功, orderNo={}, ticketCount={}",
order.getOrderNo(), result.getData().size());
}
}

View File

@@ -0,0 +1,87 @@
package pers.amos.mall.trade.liteflow.node;
import cn.hutool.core.util.RandomUtil;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Component;
import pers.amos.mall.api.dto.InventoryOperationDTO;
import pers.amos.mall.api.dto.OrderCreateDTO;
import pers.amos.mall.api.service.RouteService;
import pers.amos.mall.common.enums.OrderTypeEnum;
import pers.amos.mall.common.response.Result;
import pers.amos.mall.trade.liteflow.context.OrderContext;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* 锁定库存节点
*/
@Slf4j
@Component("lockInventory")
public class LockInventoryNode extends NodeComponent {
@DubboReference
private RouteService routeService;
@Override
public void process() throws Exception {
OrderContext context = this.getContextBean(OrderContext.class);
OrderCreateDTO dto = context.getOrderCreateDTO();
// 生成订单号
String orderNo = generateOrderNo();
log.info("开始锁定库存, orderNo={}, orderType={}, quantity={}",
orderNo, dto.getOrderType(), dto.getQuantity());
// 构建库存操作DTO
InventoryOperationDTO operationDTO = new InventoryOperationDTO();
operationDTO.setOrderNo(orderNo);
operationDTO.setOrderType(dto.getOrderType());
operationDTO.setScheduleCode(dto.getScheduleCode());
operationDTO.setRollingScheduleCode(generateRollingScheduleCode(dto));
operationDTO.setQuantity(dto.getQuantity());
// Dubbo调用Route服务锁定库存
Result<Boolean> result = routeService.lockInventory(operationDTO);
if (!result.isSuccess() || !Boolean.TRUE.equals(result.getData())) {
throw new RuntimeException("库存锁定失败:" + result.getErrorDesc());
}
// 将订单号和滚动班次编号保存到上下文
dto.setScheduleCode(dto.getScheduleCode());
if (OrderTypeEnum.ROLLING.getCode().equals(dto.getOrderType())) {
// 将生成的滚动班次编号保存到DTO供后续节点使用
context.setErrorMessage(operationDTO.getRollingScheduleCode()); // 临时存储
}
log.info("库存锁定成功, orderNo={}", orderNo);
// 将订单号保存到上下文
context.setErrorMessage(orderNo); // 临时存储订单号
}
/**
* 生成订单号
* 格式ORD + yyyyMMddHHmmssSSS + 6位随机数
*/
private String generateOrderNo() {
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"));
String random = RandomUtil.randomNumbers(6);
return "ORD" + timestamp + random;
}
/**
* 生成滚动班次编号
*/
private String generateRollingScheduleCode(OrderCreateDTO dto) {
if (OrderTypeEnum.ROLLING.getCode().equals(dto.getOrderType())) {
return dto.getRouteCode() + "-" + dto.getScheduleDate().replace("-", "") + "-RS";
}
return null;
}
}

View File

@@ -0,0 +1,58 @@
package pers.amos.mall.trade.liteflow.node;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import pers.amos.mall.api.dto.OrderCreateDTO;
import pers.amos.mall.common.enums.OrderTypeEnum;
import pers.amos.mall.trade.liteflow.context.OrderContext;
/**
* 参数校验节点
*/
@Slf4j
@Component("validateParams")
public class ValidateParamsNode extends NodeComponent {
@Override
public void process() throws Exception {
OrderContext context = this.getContextBean(OrderContext.class);
OrderCreateDTO dto = context.getOrderCreateDTO();
log.info("开始参数校验, userNo={}", dto.getUserNo());
// 校验基本参数
if (!StringUtils.hasText(dto.getUserNo())) {
throw new IllegalArgumentException("用户编号不能为空");
}
if (!StringUtils.hasText(dto.getRouteCode())) {
throw new IllegalArgumentException("线路编码不能为空");
}
if (!StringUtils.hasText(dto.getOrderType())) {
throw new IllegalArgumentException("订单类型不能为空");
}
if (dto.getQuantity() == null || dto.getQuantity() <= 0) {
throw new IllegalArgumentException("购票数量必须大于0");
}
// 根据订单类型校验特定参数
if (OrderTypeEnum.FIXED.getCode().equals(dto.getOrderType())) {
if (!StringUtils.hasText(dto.getScheduleCode())) {
throw new IllegalArgumentException("固定班次编号不能为空");
}
} else if (OrderTypeEnum.ROLLING.getCode().equals(dto.getOrderType())) {
if (!StringUtils.hasText(dto.getScheduleDate())) {
throw new IllegalArgumentException("滚动班次日期不能为空");
}
} else {
throw new IllegalArgumentException("订单类型无效");
}
log.info("参数校验通过, userNo={}", dto.getUserNo());
}
}

View File

@@ -11,7 +11,6 @@ spring:
username: root
password: root
# Jackson配置
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
@@ -44,14 +43,18 @@ dubbo:
port: 20882
registry:
address: nacos://localhost:8848
scan:
base-packages: pers.amos.mall.trade.api
consumer:
check: false
timeout: 5000
retries: 0
# RocketMQ配置
rocketmq:
name-server: localhost:9876
producer:
group: trade-producer-group
consumer:
group: trade-consumer-group
# LiteFlow配置
liteflow:
rule-source: liteflow/order-flow.el.xml
enable: true

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<!-- 下单流程 -->
<chain name="orderChain">
THEN(
validateParams,
checkInventory,
lockInventory,
createOrder,
generateTickets,
buildResult
);
</chain>
</flow>