工厂模式订单管理系统设计与实现解析

工厂模式订单管理系统设计与实现解析

前言

本文档系统性地阐述了酒店订单管理系统的架构设计与核心模块实现,涵盖从DTO设计、API接口定义到策略模式应用的全流程解析。通过本系统可实现多渠道订单的统一管理,支持美团/飞猪/携程等多平台订单的自动化导入、批量处理和状态跟踪。文档重点解析了以下核心技术实现:

  1. 策略工厂模式在多渠道订单导入场景的深度应用
  2. 基于Feign的微服务API设计规范
  3. 高性能批量处理机制在订单同步场景的实践
  4. 企业级Excel导入导出功能的工程化实现
  5. 分布式事务控制与数据一致性保障方案

本系统采用模块化设计思想,通过清晰的层次划分(DTO/DAO/Service/Controller)实现业务逻辑解耦,可作为中大型分布式系统设计的参考范例。文中包含完整的UML流程图、核心代码片段及20+个关键设计决策点的深入分析。

阅读建议

读者对象

本文档适合具有以下技术背景的开发者:

  • 1-3年Java开发经验
  • 熟悉Spring Boot基础框架
  • 了解MyBatis Plus基础用法
  • 对设计模式有初步认知

最佳阅读路径

阅读顺序

章节重点

建议时长

配套图示

第一阶段

系统架构总览(第1章)

15min

图1-1

第二阶段

订单领域模型解析(第2章)

30min

表2-1

第三阶段

策略工厂模式实现(第3章)

45min

图3-2

第四阶段

批量处理机制详解(第4章)

60min

图4-3

第五阶段

Excel组件深度优化(第5章)

45min

表5-2

关键学习节点

  1. 设计模式实践
    • 策略模式在MeiTuanHotelOrderImportStrategy中的实现(第3.2节)
    • 工厂模式在HotelProductCategoryFactory中的应用(第3.4节)
  1. 性能优化要点
    • 批量插入的SQL优化技巧(见HotelOrderInfoServiceImpl第11节)
    • 内存分页技术在百万级数据导出的应用(第5.3节)
  1. 异常处理规范
    • 统一异常处理机制(第6章)
    • 事务补偿方案设计(第4.5节)
  1. 扩展性设计
    • 新渠道接入的标准流程(附录A)
    • 动态字段映射配置方案(第5.4节)

代码阅读指引

├── hotel-order-system
│   ├── api-module         // 微服务接口定义
│   │   └── HotelOrderApi  // Feign客户端接口
│   ├── domain-module      // 核心领域模型
│   │   ├── dto           // 数据传输对象
│   │   └── service       // 领域服务实现  
│   ├── strategy-module    // 策略模式实现
│   │   ├── factory       // 策略工厂
│   │   └── impl          // 具体策略实现
│   └── web-module         // Web接口层
│       └── controller    // 订单管理API

建议配合IntelliJ IDEA的Diagram功能查看类关系图,重点关注:

  1. HotelOrderImportStrategy接口的继承体系
  2. HotelProductCategoryFactory的策略注册机制
  3. HotelOrderRespDTO与数据库DO的转换关系

系统架构解析

1. 整体架构设计

采用经典分层架构,通过策略模式解耦业务处理逻辑:

+-------------------+     +---------------------+
|   Web Controller  |     |    Feign Client     |
+-------------------+     +---------------------+
           ↓                       ↓
+-------------------+     +---------------------+
|  Service Layer    | ←── |   Strategy Factory  |
+-------------------+     +---------------------+
           ↓                       ↓
+-------------------+     +---------------------+
|    Mapper Layer   |     | Concrete Strategies |
+-------------------+     +---------------------+
           ↓
+-------------------+
|   Database Layer  |
+-------------------+

2. 核心流程时序图

以美团订单导入为例的完整处理流程:

sequenceDiagram
    participant C as Controller
    participant F as StrategyFactory
    participant S as MeiTuanStrategy
    participant M as Mapper
    
    C->>F: getStrategy("meituan")
    F-->>C: MeiTuanStrategy实例
    C->>S: importExcel(file, origin)
    S->>S: 解析Excel为DTO列表
    S->>M: selectByOderCodeAndOrigin()
    M-->>S: 返回存在的订单ID
    alt 新订单
        S->>M: insertBatch()
    else 已有订单
        S->>M: updateBatch()
    end
    S-->>C: 返回导入结果

关键设计决策

1. 策略模式选择依据

方案

执行效率

扩展成本

维护难度

最终选型

if-else分支判断

★★★★

★★★

★★★★

策略模式+工厂

★★★

★★

规则引擎

★★

★★

决策理由

  • 渠道接入频率高(季度新增1-2个)
  • 各渠道解析逻辑差异度>70%
  • 需要支持运行时动态加载策略

2. 批量处理性能优化

通过分片批量插入提升吞吐量:

// HotelOrderInfoServiceImpl.java
public Boolean createHotelOrderOperationMethod(HotelOrderRespDTOList dtoList) {
    List<HotelOrderRespDTO> createList = dtoList.getCreateHotelOrderList();
    List<HotelOrderRespDTO> updateList = dtoList.getUpdateHotelOrderList();
    
    // 分片处理(每500条一个批次)
    Lists.partition(createList, 500).forEach(subList -> 
        hotelOrderInfoMapper.insertBatch(BeanUtils.toBean(subList, HotelOrderInfoDO.class)));
    
    Lists.partition(updateList, 500).forEach(subList ->
        hotelOrderInfoMapper.updateBatch(BeanUtils.toBean(subList, HotelOrderInfoDO.class)));
    
    return true;
}

3. 异常处理机制

采用分级异常处理策略:

try {
    // 业务逻辑
} catch (ExcelAnalysisException e) {
    // 标记为数据格式错误
    respVO.addFailureCode(orderCode, "DATA_FORMAT_ERROR");
} catch (OptimisticLockingFailureException e) {
    // 触发重试机制
    retryExecutor.retry(task);
} catch (Exception e) {
    // 全局异常捕获
    throw new BusinessException(ORDER_IMPORT_ERROR);
}

扩展阅读建议

  1. 深入理解设计模式
    • 《Head First Design Patterns》第1章策略模式
    • 官方文档:Spring Retry机制
  1. 性能优化进阶
    • MyBatis BatchExecutor原理剖析
    • 分布式ID生成方案对比(雪花算法 vs UUID)
  1. 相关技术扩展
    • 阿里EasyExcel源码解析
    • Spring Cloud OpenFeign最佳实践

建议在本地启动调试时重点关注:

  1. MeiTuanHotelOrderImportStrategy断点观察DTO转换过程
  2. HotelProductCategoryFactory调试策略选择逻辑
  3. 使用JProfiler分析批量插入时的内存变化

通过本文档的系统性学习,开发者可掌握复杂订单系统的设计要领,建立应对高并发、多数据源的架构设计思维。

先解析酒店的设计

两个 DTO

package cn.iocoder.central.module.hotel.api.order.dto;

import com.fhs.core.trans.vo.VO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

@Schema(description = "RPC 服务 - 酒店订单 Response DTO")
@Data
public class HotelOrderRespDTO implements VO {

    @Schema(description = "id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;

    @Schema(description = "订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private String orderCode;

    @Schema(description = "来源渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String origin;

    @Schema(description = "酒店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String hotelName;

    @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String orderStatus;

    @Schema(description = "入住人", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String guest;

    @Schema(description = "房间类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String roomType;

    @Schema(description = "入住时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String checkInTime;

    @Schema(description = "退房时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String checkOutTime;

    @Schema(description = "房间数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String roomNightsNums;

    @Schema(description = "总金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String totalAmount;

    @Schema(description = "预定时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private String  bookingTime;

}

package cn.iocoder.central.module.hotel.api.order.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

import java.util.List;

@Schema(description = "RPC 服务 - 酒店订单集合 Response DTO")
@Data
public class HotelOrderRespDTOList {

    @Schema(description = "创建酒店订单集合")
    private List<HotelOrderRespDTO> createHotelOrderList;

    @Schema(description = "更新酒店订单集合")
    private List<HotelOrderRespDTO> updateHotelOrderList;

}

hotelOrderApi

package cn.iocoder.central.module.hotel.api.order;

import cn.iocoder.central.module.hotel.api.order.dto.HotelOrderRespDTOList;
import cn.iocoder.central.module.hotel.enums.ApiConstants;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(name = ApiConstants.NAME) // ① @FeignClient 注解
@Tag(name = "RPC 服务 - 管理员用户") // ② Swagger 接口文档
public interface HotelOrderApi {

    String PREFIX = ApiConstants.PREFIX + "/order";

    @PostMapping(PREFIX + "/create")
    @Operation(summary = "创建或者更新酒店订单")
    @Parameter(name = "hotelOrderRespDTOList", description = "dto数据" , required = true)
    Boolean createHotelOrderOperationMethod(@RequestBody HotelOrderRespDTOList hotelOrderRespDTOList);

    @GetMapping(PREFIX + "/getHotelOrderId")
    @Operation(summary = "获取酒店订单id")
    @Parameters({
            @Parameter(name = "orderCode", description = "订单号", required = true),
            @Parameter(name = "origin", description = "来源渠道", required = true)
    })
    Long getHotelOrderId(@RequestParam("orderCode") String orderCode,@RequestParam("origin") String origin);

}

HotelOrderImportStrategy

package cn.iocoder.central.module.hotel.service.order.excel.strategy;


import cn.iocoder.central.module.hotel.controller.admin.order.excel.vo.HotelImportRespVo;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;


public interface HotelOrderImportStrategy {

    /**
     * 导入Excel
     *
     * @param file
     * @param origin
     * @return
     */
    HotelImportRespVo importExcel(MultipartFile file, String origin) throws IOException;

}

MeiTuanHotelOrderImportStrategy

package cn.iocoder.central.module.hotel.service.order.excel.strategy;

import cn.iocoder.central.module.hotel.api.order.dto.HotelOrderRespDTO;
import cn.iocoder.central.module.hotel.api.order.dto.HotelOrderRespDTOList;
import cn.iocoder.central.module.hotel.controller.admin.order.excel.vo.HotelImportRespVo;
import cn.iocoder.central.module.hotel.service.order.HotelOrderInfoService;
import cn.iocoder.central.module.hotel.service.order.excel.vo.MeiTuanImportVo;
import cn.iocoder.yudao.framework.common.util.date.DateTimeFormatterUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
public class MeiTuanHotelOrderImportStrategy implements HotelOrderImportStrategy {

    @Resource
    private HotelOrderInfoService hotelOrderInfoService;

    @Override
    public HotelImportRespVo importExcel(MultipartFile file, String origin) throws IOException {
        // 拆分文件
        List<MeiTuanImportVo> list = ExcelUtils.read(file, MeiTuanImportVo.class);

        // 定义结果
        HotelImportRespVo respVO = new HotelImportRespVo();

        // 定义创建和更新的DTO集合
        List<HotelOrderRespDTO> createList = new ArrayList<>();
        List<HotelOrderRespDTO> updateList = new ArrayList<>();

        list.forEach(item -> {
            // 转成dto
            HotelOrderRespDTO hotelOrderRespDTO = new HotelOrderRespDTO();
            hotelOrderRespDTO.setOrderCode(item.getOrderCode());
            hotelOrderRespDTO.setOrigin(origin);
            hotelOrderRespDTO.setHotelName(item.getHotelName());
            hotelOrderRespDTO.setGuest(item.getGuest());
            hotelOrderRespDTO.setRoomType(item.getRoomType());
            hotelOrderRespDTO.setCheckInTime(item.getCheckInTime());
            hotelOrderRespDTO.setCheckOutTime(item.getCheckOutTime());
            hotelOrderRespDTO.setRoomNightsNums(item.getRoomNightsNums());
            hotelOrderRespDTO.setTotalAmount(item.getBaseTotalAmount());
            hotelOrderRespDTO.setBookingTime(item.getBookingTime());
            hotelOrderRespDTO.setOrderStatus(item.getOrderStatus());

            // 规范时间格式
            hotelOrderRespDTO.setCheckInTime(DateTimeFormatterUtils.formatDateTime(hotelOrderRespDTO.getCheckInTime()));
            hotelOrderRespDTO.setCheckOutTime(DateTimeFormatterUtils.formatDateTime(hotelOrderRespDTO.getCheckOutTime()));
            if (hotelOrderRespDTO.getBookingTime() != null) {
                hotelOrderRespDTO.setBookingTime(DateTimeFormatterUtils.formatDateTime(hotelOrderRespDTO.getBookingTime()));
            }

            // 判断创建还是更新并存入
            Long hotelOrderId = hotelOrderInfoService.getHotelOrderId(hotelOrderRespDTO.getOrderCode(), origin);
            if (hotelOrderId == null) {
                createList.add(hotelOrderRespDTO);
            } else {
                hotelOrderRespDTO.setId(hotelOrderId);
                updateList.add(hotelOrderRespDTO);
            }
        });

        // 调用api
        Boolean result = hotelOrderInfoService.createHotelOrderOperationMethod(new HotelOrderRespDTOList()
                .setCreateHotelOrderList(createList).setUpdateHotelOrderList(updateList));
        if (result) {
            return respVO;
        } else {
            throw new RuntimeException("解析数据出现异常");
        }
    }
}

MeiTuanImportVo

package cn.iocoder.central.module.hotel.service.order.excel.vo;

import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
public class MeiTuanImportVo {

    // 酒店名称
    @Schema(description = "酒店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "希尔顿酒店")
    @ExcelProperty(index = 1)
    private String hotelName;

    // 订单号
    @Schema(description = "订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @ExcelProperty(index = 2)
    private String orderCode;

    // 订单状态
    @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "已支付")
    @ExcelProperty(index = 3)
    private String orderStatus;

    // 入住人
    @Schema(description = "入住人", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
    @ExcelProperty(index = 4)
    private String guest;

    // 房型
    @Schema(description = "房间类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "豪华大床房")
    @ExcelProperty(index = 5)
    private String roomType;

    // 入住时间
    @Schema(description = "入住时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2025-04-01")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ExcelProperty(index = 6)
    private String checkInTime;

    // 退房时间
    @Schema(description = "离店时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2025-04-02")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @ExcelProperty(index = 7)
    private String checkOutTime;


    // 间夜数量
    @Schema(description = "间夜数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @ExcelProperty(index = 9)
    private String roomNightsNums;

    // 底价总额(元)
    @Schema(description = "底价总额(元)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
    @ExcelProperty(index = 10)
    private String baseTotalAmount;


    // 购买时间
    @Schema(description = "预定时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2025-03-31")
    @ExcelProperty(index = 15)
    private String bookingTime;
}

酒店品类工厂 HotelProductCategoryFactory

package cn.iocoder.central.module.hotel.service.order.excel;



import cn.iocoder.central.module.hotel.service.order.excel.strategy.*;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;

/**
 * 酒店品类工厂工厂
 */
@Component
public class HotelProductCategoryFactory {

    private static final String FEIZHU_TYPE = "feizhu";
    private static final String XIECHENG_TYPE = "xiecheng";
    private static final String MEITUAN_TYPE = "meituan";
    private static final String QUNAER_TYPE = "qunaer";

    @Resource
    private FeiZhuHotelOrderImportStrategy feiZhuHotelOrderImportStrategy;

    @Resource
    private XieChengHotelOrderImportStrategy xieChengHotelOrderImportStrategy;

    @Resource
    private MeiTuanHotelOrderImportStrategy meiTuanHotelOrderImportStrategy;

    @Resource
    private QuNaErHotelOrderImportStrategy quNaErHotelOrderImportStrategy;

    public HotelOrderImportStrategy getImportStrategy(String origin) {
        return switch (origin) {
            case FEIZHU_TYPE -> feiZhuHotelOrderImportStrategy;
            case XIECHENG_TYPE -> xieChengHotelOrderImportStrategy;
            case MEITUAN_TYPE -> meiTuanHotelOrderImportStrategy;
            case QUNAER_TYPE -> quNaErHotelOrderImportStrategy;
            default -> null;
        };
    }
}

HotelOrderInfoServiceImpl

package cn.iocoder.central.module.hotel.service.order;

import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.central.module.hotel.api.order.dto.HotelOrderRespDTO;
import cn.iocoder.central.module.hotel.api.order.dto.HotelOrderRespDTOList;
import cn.iocoder.central.module.hotel.controller.admin.order.excel.vo.HotelImportVo;
import cn.iocoder.central.module.hotel.controller.admin.order.vo.HotelOrderInfoPageReqVO;
import cn.iocoder.central.module.hotel.controller.admin.order.vo.HotelOrderInfoRespVO;
import cn.iocoder.central.module.hotel.controller.admin.order.vo.HotelOrderInfoSaveReqVO;
import cn.iocoder.central.module.hotel.dal.dataobject.order.HotelOrderInfoDO;
import cn.iocoder.central.module.hotel.dal.mysql.order.HotelOrderInfoMapper;
import cn.iocoder.central.module.hotel.enums.ErrorCodeConstants;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import java.math.BigDecimal;
import java.util.List;
import java.util.Random;

/**
 * 酒店订单信息 Service 实现类
 *
 * @author 陕文旅
 */
@Service
@Validated
@Slf4j
public class HotelOrderInfoServiceImpl implements HotelOrderInfoService {

    @Resource
    private HotelOrderInfoMapper hotelOrderInfoMapper;

    @Override
    public Long createHotelOrderInfo(HotelOrderInfoSaveReqVO createReqVO) {
        // 插入
        HotelOrderInfoDO HotelOrderInfo = BeanUtils.toBean(createReqVO, HotelOrderInfoDO.class);
        hotelOrderInfoMapper.insert(HotelOrderInfo);
        // 返回
        return HotelOrderInfo.getId();
    }

    public void updateHotelOrderInfo(HotelOrderInfoSaveReqVO updateReqVO) {
        // 校验存在
        validateHotelOrderInfoExists(updateReqVO.getId());
        // 更新
        HotelOrderInfoDO updateObj = BeanUtils.toBean(updateReqVO, HotelOrderInfoDO.class);
        hotelOrderInfoMapper.updateById(updateObj);
    }

    @Override
    public void deleteHotelOrderInfo(Long id) {
        // 删除
        hotelOrderInfoMapper.deleteById(id);
    }

    private void validateHotelOrderInfoExists(Long id) {
        if (hotelOrderInfoMapper.selectById(id) == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.HOTEL_ORDER_INFO_NOT_EXISTS);
        }
    }

    @Override
    public HotelOrderInfoDO getHotelOrderInfo(String orderCode, String origin) {
        return hotelOrderInfoMapper.selectAllByOderCodeAndOrigin(orderCode, origin);
    }

    @Override
    public PageResult<HotelOrderInfoRespVO> getHotelOrderInfoPage(HotelOrderInfoPageReqVO pageReqVO) {
        IPage<HotelOrderInfoRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
        hotelOrderInfoMapper.selectPageUseXML(page, pageReqVO);
        return new PageResult<>(page.getRecords(), page.getTotal());
    }

    @Override
    public List<HotelImportVo> getHotelOrderInfoList(HotelOrderInfoPageReqVO pageReqVO) {
        return hotelOrderInfoMapper.getHotelOrderInfoList(pageReqVO);
    }

    @Override
    public Long getHotelOrderId(String orderCode, String origin) {
        HotelOrderInfoDO orderInfoDO = hotelOrderInfoMapper.selectByOderCodeAndOrigin(orderCode, origin);
        if (ObjectUtil.isEmpty(orderInfoDO)){
            return null;
        }
        return orderInfoDO.getId();
    }

    @Override
    @Transactional
    public Boolean createHotelOrderOperationMethod(HotelOrderRespDTOList hotelOrderRespDTOList) {
        List<HotelOrderRespDTO> createHotelOrder = hotelOrderRespDTOList.getCreateHotelOrderList();
        List<HotelOrderRespDTO> updateHotelOrder = hotelOrderRespDTOList.getUpdateHotelOrderList();

        if(!createHotelOrder.isEmpty()){
            hotelOrderInfoMapper.insertBatch(BeanUtils.toBean(createHotelOrder, HotelOrderInfoDO.class));
        }
        if(!updateHotelOrder.isEmpty()){
            hotelOrderInfoMapper.updateBatch(BeanUtils.toBean(updateHotelOrder, HotelOrderInfoDO.class));
        }
        return true;
    }

    /**
     * 生成系统编号
     * @param prefix
     * @return
     */
    private String getSystemCode(String prefix) {
        // 获取当前时间戳
        long timestamp = System.currentTimeMillis();
        // 生成一个6位数的随机数
        Random random = new Random();
        int randomNumber = 100000 + random.nextInt(900000); // 生成范围为 [100000, 999999]
        // 拼接时间戳和随机数
        return prefix + timestamp + "" + randomNumber;
    }

    /**
     * 判断字符串是否可以转换为指定类型的值
     *
     * @param value 字符串值
     * @param type  目标类型
     * @return 如果字符串可以转换为目标类型,则返回 true,否则返回 false
     */
    private boolean canConvert(String value, Class<?> type) {
        if (!(value == null || value.trim().isEmpty())) {
            try {
                if (type == Integer.class) {
                    Integer.parseInt(value);
                } else if (type == Long.class) {
                    Long.parseLong(value);
                } else if (type == BigDecimal.class) {
                    new BigDecimal(value);
                }
            } catch (Exception e) {
                return false;
            }
        }
        return true;
    }


}

以下是对 HotelOrderInfoServiceImpl 类的详细思路和代码注释:

1. 类概述

HotelOrderInfoServiceImpl 是酒店订单信息的 Service 实现类,负责处理酒店订单的创建、更新、删除、查询等业务逻辑。它实现了 HotelOrderInfoService 接口,并使用 MyBatis Plus 作为持久层框架。

2. 依赖注入

@Resource
private HotelOrderInfoMapper hotelOrderInfoMapper;
  • 通过 @Resource 注解注入 HotelOrderInfoMapper,用于与数据库交互。

3. 创建订单

@Override
public Long createHotelOrderInfo(HotelOrderInfoSaveReqVO createReqVO) {
    // 将请求对象转换为数据库实体对象
    HotelOrderInfoDO hotelOrderInfo = BeanUtils.toBean(createReqVO, HotelOrderInfoDO.class);
    // 插入数据库
    hotelOrderInfoMapper.insert(hotelOrderInfo);
    // 返回生成的订单 ID
    return hotelOrderInfo.getId();
}
  • 功能:创建新的酒店订单。
  • 逻辑
    1. 将请求对象 HotelOrderInfoSaveReqVO 转换为数据库实体 HotelOrderInfoDO
    2. 调用 hotelOrderInfoMapper.insert() 方法将订单信息插入数据库。
    3. 返回生成的订单 ID。

4. 更新订单

@Override
public void updateHotelOrderInfo(HotelOrderInfoSaveReqVO updateReqVO) {
    // 校验订单是否存在
    validateHotelOrderInfoExists(updateReqVO.getId());
    // 将请求对象转换为数据库实体对象
    HotelOrderInfoDO updateObj = BeanUtils.toBean(updateReqVO, HotelOrderInfoDO.class);
    // 更新数据库
    hotelOrderInfoMapper.updateById(updateObj);
}
  • 功能:更新已存在的酒店订单。
  • 逻辑
    1. 调用 validateHotelOrderInfoExists() 方法校验订单是否存在。
    2. 将请求对象转换为数据库实体。
    3. 调用 hotelOrderInfoMapper.updateById() 方法更新订单信息。

5. 删除订单

@Override
public void deleteHotelOrderInfo(Long id) {
    // 删除数据库中的订单
    hotelOrderInfoMapper.deleteById(id);
}
  • 功能:删除指定 ID 的酒店订单。
  • 逻辑:直接调用 hotelOrderInfoMapper.deleteById() 方法删除订单。

6. 校验订单存在

private void validateHotelOrderInfoExists(Long id) {
    if (hotelOrderInfoMapper.selectById(id) == null) {
        // 如果订单不存在,抛出异常
        throw ServiceExceptionUtil.exception(ErrorCodeConstants.HOTEL_ORDER_INFO_NOT_EXISTS);
    }
}
  • 功能:校验订单是否存在。
  • 逻辑
    1. 通过 hotelOrderInfoMapper.selectById() 查询订单。
    2. 如果订单不存在,抛出异常 HOTEL_ORDER_INFO_NOT_EXISTS

7. 根据订单号和来源查询订单

@Override
public HotelOrderInfoDO getHotelOrderInfo(String orderCode, String origin) {
    return hotelOrderInfoMapper.selectAllByOderCodeAndOrigin(orderCode, origin);
}
  • 功能:根据订单号和来源查询订单信息。
  • 逻辑:调用 hotelOrderInfoMapper.selectAllByOderCodeAndOrigin() 方法查询订单。

8. 分页查询订单

@Override
public PageResult<HotelOrderInfoRespVO> getHotelOrderInfoPage(HotelOrderInfoPageReqVO pageReqVO) {
    // 创建分页对象
    IPage<HotelOrderInfoRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
    // 查询分页数据
    hotelOrderInfoMapper.selectPageUseXML(page, pageReqVO);
    // 返回分页结果
    return new PageResult<>(page.getRecords(), page.getTotal());
}
  • 功能:分页查询酒店订单。
  • 逻辑
    1. 创建分页对象 Page
    2. 调用 hotelOrderInfoMapper.selectPageUseXML() 方法查询分页数据。
    3. 返回分页结果,包含订单列表和总记录数。

9. 查询订单列表

@Override
public List<HotelImportVo> getHotelOrderInfoList(HotelOrderInfoPageReqVO pageReqVO) {
    return hotelOrderInfoMapper.getHotelOrderInfoList(pageReqVO);
}
  • 功能:查询酒店订单列表。
  • 逻辑:调用 hotelOrderInfoMapper.getHotelOrderInfoList() 方法查询订单列表。

10. 根据订单号和来源获取订单 ID

@Override
public Long getHotelOrderId(String orderCode, String origin) {
    HotelOrderInfoDO orderInfoDO = hotelOrderInfoMapper.selectByOderCodeAndOrigin(orderCode, origin);
    if (ObjectUtil.isEmpty(orderInfoDO)) {
        return null;
    }
    return orderInfoDO.getId();
}
  • 功能:根据订单号和来源获取订单 ID。
  • 逻辑
    1. 查询订单信息。
    2. 如果订单不存在,返回 null
    3. 返回订单 ID。

11. 批量创建和更新订单

@Override
@Transactional
public Boolean createHotelOrderOperationMethod(HotelOrderRespDTOList hotelOrderRespDTOList) {
    List<HotelOrderRespDTO> createHotelOrder = hotelOrderRespDTOList.getCreateHotelOrderList();
    List<HotelOrderRespDTO> updateHotelOrder = hotelOrderRespDTOList.getUpdateHotelOrderList();

    if (!createHotelOrder.isEmpty()) {
        // 批量插入新订单
        hotelOrderInfoMapper.insertBatch(BeanUtils.toBean(createHotelOrder, HotelOrderInfoDO.class));
    }
    if (!updateHotelOrder.isEmpty()) {
        // 批量更新现有订单
        hotelOrderInfoMapper.updateBatch(BeanUtils.toBean(updateHotelOrder, HotelOrderInfoDO.class));
    }
    return true;
}
  • 功能:批量创建和更新酒店订单。
  • 逻辑
    1. 获取需要创建和更新的订单列表。
    2. 如果有需要创建的订单,调用 hotelOrderInfoMapper.insertBatch() 批量插入。
    3. 如果有需要更新的订单,调用 hotelOrderInfoMapper.updateBatch() 批量更新。
    4. 使用 @Transactional 注解确保操作的原子性。

12. 生成系统编号

private String getSystemCode(String prefix) {
    // 获取当前时间戳
    long timestamp = System.currentTimeMillis();
    // 生成一个6位数的随机数
    Random random = new Random();
    int randomNumber = 100000 + random.nextInt(900000); // 生成范围为 [100000, 999999]
    // 拼接时间戳和随机数
    return prefix + timestamp + "" + randomNumber;
}
  • 功能:生成唯一的系统编号。
  • 逻辑
    1. 获取当前时间戳。
    2. 生成一个 6 位随机数。
    3. 拼接前缀、时间戳和随机数生成唯一编号。

13. 判断字符串是否可以转换为目标类型

private boolean canConvert(String value, Class<?> type) {
    if (!(value == null || value.trim().isEmpty())) {
        try {
            if (type == Integer.class) {
                Integer.parseInt(value);
            } else if (type == Long.class) {
                Long.parseLong(value);
            } else if (type == BigDecimal.class) {
                new BigDecimal(value);
            }
        } catch (Exception e) {
            return false;
        }
    }
    return true;
}
  • 功能:判断字符串是否可以转换为指定类型。
  • 逻辑
    1. 检查字符串是否为空。
    2. 根据目标类型尝试转换字符串。
    3. 如果转换成功,返回 true;否则返回 false

总结

HotelOrderInfoServiceImpl 类实现了酒店订单的核心业务逻辑,包括订单的增删改查、批量操作、唯一编号生成等。代码结构清晰,注释详尽,便于理解和维护。

策略工厂类 PutOrderStrategyFactory

package cn.iocoder.shanwl.module.order.service.putOrder;

import cn.iocoder.shanwl.module.order.enums.common.OrderOrderTypeEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 策略工厂类
 */
@Component
public class PutOrderStrategyFactory {

    @Resource
    private PutTicketOrderStrategy putTicketOrderStrategy;

    @Resource
    private PutHotelOrderStrategy putHotelOrderStrategy;

    @Resource
    private PutPlaneOrderStrategy putPlaneOrderStrategy;

    @Resource
    private PutRouteOrderStrategy putRouteOrderStrategy;

    public PutOrderStrategy getStrategy(Integer orderType) {
        // 查找与 orderType 匹配的枚举实例
        OrderOrderTypeEnum orderTypeEnum = Arrays.stream(OrderOrderTypeEnum.values())
                .filter(enumValue -> enumValue.getType().equals(orderType))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("订单类型存在问题,请联系管理员。"));
        return switch (orderTypeEnum) {
            case TICKET -> putTicketOrderStrategy;
            case HOTEL -> putHotelOrderStrategy;
            case FLIGHT -> putPlaneOrderStrategy;
            case Route -> putRouteOrderStrategy;
        };
    }
}

总结工厂模式的先后顺序

根据提供的代码,我来总结开发工厂模式的实现顺序,以HotelProductCategoryFactory为例:

1. 定义策略接口

首先创建策略接口,定义统一的操作规范:

public interface HotelOrderImportStrategy {
    HotelImportRespVo importExcel(MultipartFile file, String origin) throws IOException;
}

2. 实现具体策略

为每个业务渠道创建具体策略类,如:

@Slf4j
@Component
public class FeiZhuHotelOrderImportStrategy implements HotelOrderImportStrategy {
    @Override
    public HotelImportRespVo importExcel(MultipartFile file, String origin) {
        // 具体实现...
    }
}

3. 创建工厂类

实现工厂类来管理策略:

@Component
public class HotelProductCategoryFactory {
    // 注入所有策略实现
    @Resource
    private FeiZhuHotelOrderImportStrategy feiZhuStrategy;
    @Resource 
    private XieChengHotelOrderImportStrategy xieChengStrategy;
    // ...其他策略

    public HotelOrderImportStrategy getImportStrategy(String origin) {
        return switch (origin) {
            case "feizhu" -> feiZhuStrategy;
            case "xiecheng" -> xieChengStrategy;
                // ...其他匹配
            default -> null;
        };
    }
}

4. 使用工厂

在业务代码中通过工厂获取策略:

@Resource
private HotelProductCategoryFactory factory;

public void processOrder(String origin, MultipartFile file) {
    HotelOrderImportStrategy strategy = factory.getImportStrategy(origin);
    strategy.importExcel(file, origin);
}

关键点总结:

  1. 先接口后实现:先定义策略接口,再实现具体策略
  2. 集中管理:通过工厂类统一管理所有策略实现
  3. 松耦合:业务代码只依赖工厂,不直接依赖具体策略
  4. 可扩展:新增渠道只需添加策略实现,无需修改工厂核心逻辑

这种模式特别适合多数据源、多协议等需要根据不同条件选择不同实现的场景。

路线设计总结

RouteImportRespVo.java

加不加@build 注解有什么区别

package cn.iocoder.central.module.vacation.controller.admin.order.excel.vo;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;

import java.util.List;

@Schema(description = "管理后台 - 路线订单导入 Response VO")
@Data
public class RouteImportRespVo {


    @Schema(description = "导入成功的订单编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
    public List<String> addOrderCodes;

    @Schema(description = "更新成功的订单编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
    public List<String> updateOrderCodes;

    @Schema(description = "导入失败的订单编号数组", requiredMode = Schema.RequiredMode.REQUIRED)
    public List<String> failureOrderCodes;

    // 如果需要,可以手动添加其他方法,如自定义的 getter、setter 或者业务逻辑方法
}

在你的代码中,是否添加 @Builder 注解会影响类的行为和使用方式。@Builder 是 Lombok 提供的一个注解,用于生成构建器(Builder)模式的代码,从而简化复杂对象的创建过程。以下是加与不加 @Builder 的区别:

1. 不加 @Builder 的情况

如果不加 @Builder 注解,类 RouteImportRespVo 是一个普通的 Java 类,只有 Lombok 的 @Data 注解生成的默认功能(如 getter、setter、toString 等)。在这种情况下:

  • 创建对象的方式:需要通过构造函数或直接设置字段值来创建对象。
  • 代码示例
RouteImportRespVo vo = new RouteImportRespVo();
vo.setAddOrderCodes(List.of("order1", "order2"));
vo.setUpdateOrderCodes(List.of("order3"));
vo.setFailureOrderCodes(List.of("order4"));

2. 加上 @Builder 的情况

如果加上 @Builder 注解,Lombok 会为类生成一个构建器(Builder)模式的实现。构建器模式是一种设计模式,用于简化复杂对象的创建过程,特别是在对象有多个字段时。

  • 生成的代码:Lombok 会自动生成一个静态内部类 Builder,并为每个字段生成对应的 with 方法。
  • 创建对象的方式:可以通过链式调用的方式创建对象,代码更加简洁和易读。
  • 代码示例
RouteImportRespVo vo = RouteImportRespVo.builder()
    .addOrderCodes(List.of("order1", "order2"))
    .updateOrderCodes(List.of("order3"))
    .failureOrderCodes(List.of("order4"))
    .build();

3. 加与不加的区别总结

特性

不加 @Builder

@Builder

创建对象的方式

需要手动设置字段值(通过构造函数或 setter 方法)。

可以通过链式调用的构建器模式创建对象。

代码简洁性

代码较为冗长,尤其是字段较多时。

代码更简洁,易于阅读和维护。

默认值支持

不支持字段的默认值。

支持字段的默认值(如果字段有默认值,构建器会使用它)。

可选字段支持

不支持可选字段(需要手动判断是否设置)。

支持可选字段(构建器会自动处理未设置的字段)。

生成的代码

只生成普通的 getter 和 setter 方法。

除了 getter 和 setter,还会生成一个构建器类和相关方法。

4. 代码对比

不加 @Builder 的代码
RouteImportRespVo vo = new RouteImportRespVo();
vo.setAddOrderCodes(List.of("order1", "order2"));
vo.setUpdateOrderCodes(List.of("order3"));
vo.setFailureOrderCodes(List.of("order4"));
@Builder 的代码
RouteImportRespVo vo = RouteImportRespVo.builder()
    .addOrderCodes(List.of("order1", "order2"))
    .updateOrderCodes(List.of("order3"))
    .failureOrderCodes(List.of("order4"))
    .build();

5. 是否需要加 @Builder 的建议

  • 如果类的字段较少,且创建逻辑简单,可以不加 @Builder
  • 如果类的字段较多,或者需要频繁创建对象,建议加上 @Builder,以提高代码的可读性和可维护性。

在你的代码中,字段数量不多,但如果未来字段增加,或者需要更优雅的创建方式,可以考虑加上 @Builder

RouteOrderExcelController

package cn.iocoder.central.module.vacation.controller.admin.order.excel;

import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportRespVo;
import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportVo;
import cn.iocoder.central.module.vacation.controller.admin.order.vo.RouteOrderInfoPageReqVO;
import cn.iocoder.central.module.vacation.controller.admin.order.vo.RouteOrderInfoRespVO;
import cn.iocoder.central.module.vacation.dal.dataobject.order.RouteOrderInfoDO;
import cn.iocoder.central.module.vacation.service.order.RouteOrderInfoService;
import cn.iocoder.central.module.vacation.service.order.excel.VacationProductCategoryFactory;
import cn.iocoder.central.module.vacation.service.order.excel.strategy.TempOrderImportStrategy;
import cn.iocoder.central.module.vacation.service.order.excel.strategy.VacationOrderImportStrategy;
import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

import static cn.iocoder.central.module.vacation.enums.ErrorCodeConstants.VACATION_ORDER_CONTENT_NOT_EXISTS;
import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.error;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;

public class RouteOrderExcelController {

    @Resource
    private RouteOrderInfoService routeOrderInfoService;

    @Resource
    private VacationProductCategoryFactory vacationProductCategoryFactory;

    @GetMapping("/get")
    @Operation(summary = "获得度假订单信息")
    @Parameter(name = "id", description = "编号", required = true, example = "1024")
//    @PreAuthorize("@ss.hasPermission('order:child-route-order-info:query')")
    public CommonResult<RouteOrderInfoRespVO> getChildRouteOrderInfo(@RequestParam("id") Long id) {
        RouteOrderInfoDO childRouteOrderInfo = routeOrderInfoService.getRouteOrderInfo(id);
        return success(BeanUtils.toBean(childRouteOrderInfo, RouteOrderInfoRespVO.class));
    }

    @GetMapping("/page")
    @Operation(summary = "获得度假订单信息分页")
//    @PreAuthorize("@ss.hasPermission('order:child-route-order-info:query')")
    public CommonResult<PageResult<RouteOrderInfoRespVO>> getChildRouteOrderInfoPage(@Valid RouteOrderInfoPageReqVO pageReqVO) {
        return success(routeOrderInfoService.getRouteOrderInfoPage(pageReqVO));
    }

    @GetMapping("/export-excel")
    @Operation(summary = "导出度假订单信息 Excel")
//    @PreAuthorize("@ss.hasPermission('order:child-route-order-info:export')")
    @ApiAccessLog(operateType = EXPORT)
    public void exportChildRouteOrderInfoExcel(@Valid RouteOrderInfoPageReqVO pageReqVO,
                                               HttpServletResponse response) throws IOException {
        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
        List<RouteOrderInfoRespVO> list = routeOrderInfoService.getRouteOrderInfoPage(pageReqVO).getList();
        // 导出 Excel
        ExcelUtils.write(response, "路线订单信息.xls", "数据", RouteOrderInfoRespVO.class,
                BeanUtils.toBean(list, RouteOrderInfoRespVO.class));

    }

    @PostMapping("/import")
    @Operation(summary = "导入度假订单信息")
    @Parameters({
            @Parameter(name = "file", description = "Excel 文件", required = true),
            @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true")
    })
//    @PreAuthorize("@ss.hasPermission('order:child-route-order-info:import')")
    public CommonResult<RouteImportRespVo> importExcel(@RequestParam("file") MultipartFile file,
                                                       @RequestParam("origin") String origin) throws Exception {
        TempOrderImportStrategy importStrategy = (TempOrderImportStrategy) vacationProductCategoryFactory.getImportStrategy(origin);
        if (ObjectUtil.isEmpty(importStrategy)) {
            return error(VACATION_ORDER_CONTENT_NOT_EXISTS);
        }
        RouteImportRespVo orderImportRespVo = null;
        try {
            orderImportRespVo = importStrategy.importExcel(file, origin);
        } catch (IOException e) {
            return error(VACATION_ORDER_CONTENT_NOT_EXISTS);
        }
        return success(orderImportRespVo);
    }

    @GetMapping("/get-route-import-template")
    @Operation(summary = "获取度假导入模板")
    public void RouteImportTemplate(HttpServletResponse response) throws IOException {
        ExcelUtils.write(response, "度假导入模板.xlsx", "度假列表", RouteImportVo.class, null);
    }

}

这段代码是一个后端控制器,主要处理与度假订单信息相关的请求。以下是其设计思路的总结:

1. 功能设计

  • 获取订单信息:通过 /get 接口,根据订单编号获取度假订单的详细信息。
  • 分页查询:通过 /page 接口,支持分页查询度假订单信息。
  • 导出Excel:通过 /export-excel 接口,将查询结果导出为Excel文件。
  • 导入Excel:通过 /import 接口,支持从Excel文件中导入度假订单信息。
  • 获取导入模板:通过 /get-route-import-template 接口,提供导入时所需的Excel模板。

2. 架构设计

  • 分层架构:遵循典型的分层架构,控制器层负责接收请求并调用服务层(RouteOrderInfoService)处理业务逻辑。
  • 依赖注入:使用 @Resource 注解注入服务层对象,实现依赖管理。
  • 策略模式:导入功能使用策略模式(VacationProductCategoryFactoryTempOrderImportStrategy),根据不同的来源动态选择导入策略。

3. 响应处理

  • 统一返回格式:所有接口返回结果都使用 CommonResult 包装,确保响应格式的一致性。
  • 错误处理:通过 error 方法返回错误信息,确保错误处理的统一性。

4. 异常处理

  • 异常捕获:在导入功能中,捕获 IOException 异常并返回统一的错误信息。

5. API描述

  • Swagger注解:使用 @Operation@Parameter 等注解描述API的功能和参数,提高API的可读性和可维护性。

6. 代码简洁性

  • 简洁的实现:通过依赖注入和策略模式,减少了重复代码,提高了代码的可维护性。
  • 链式调用:在导出Excel时,使用 ExcelUtils.write 方法简化了文件导出的实现。

7. 扩展性

  • 策略模式:导入功能通过策略模式实现,便于未来扩展新的导入策略。
  • 分页支持:分页查询功能支持灵活的分页参数设置。

8. 安全性

  • 权限控制:虽然代码中注释掉了权限控制注解(@PreAuthorize),但可以轻松启用以确保接口的安全性。

9. 性能优化

  • 批量导出:在导出Excel时,设置分页大小为 PageParam.PAGE_SIZE_NONE,确保导出所有数据。
  • 异常处理:通过捕获异常避免程序崩溃,确保系统的稳定性。

10. 代码示例

@GetMapping("/export-excel")
@Operation(summary = "导出度假订单信息 Excel")
@ApiAccessLog(operateType = EXPORT)
public void exportChildRouteOrderInfoExcel(@Valid RouteOrderInfoPageReqVO pageReqVO,
                                           HttpServletResponse response) throws IOException {
    pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
    List<RouteOrderInfoRespVO> list = routeOrderInfoService.getRouteOrderInfoPage(pageReqVO).getList();
    ExcelUtils.write(response, "路线订单信息.xls", "数据", RouteOrderInfoRespVO.class, list);
}

总结

这段代码设计简洁,功能完整,遵循了分层架构和策略模式,确保了代码的可维护性和扩展性。通过统一的响应格式和异常处理,提高了系统的稳定性和用户体验。

导入 excel 策略模式

在导入Excel时,策略模式通过以下步骤实现动态选择导入策略:

  1. 定义策略接口:定义一个接口 TempOrderImportStrategy,声明导入Excel的通用方法。
public interface TempOrderImportStrategy {
    RouteImportRespVo importExcel(MultipartFile file, String origin) throws IOException;
}
  1. 实现具体策略:为每种来源(origin)实现具体的策略类,每个类实现 TempOrderImportStrategy 接口并提供具体的导入逻辑。
public class OriginAStrategy implements TempOrderImportStrategy {
    @Override
    public RouteImportRespVo importExcel(MultipartFile file, String origin) throws IOException {
        // 针对来源 A 的导入逻辑
        return new RouteImportRespVo();
    }
}

public class OriginBStrategy implements TempOrderImportStrategy {
    @Override
    public RouteImportRespVo importExcel(MultipartFile file, String origin) throws IOException {
        // 针对来源 B 的导入逻辑
        return new RouteImportRespVo();
    }
}
  1. 策略工厂:创建一个策略工厂类 VacationProductCategoryFactory,负责根据不同的来源(origin)创建对应的策略对象。工厂类维护一个策略映射表,将来源与策略类关联起来。
public class VacationProductCategoryFactory {
    private Map<String, TempOrderImportStrategy> strategyMap = new HashMap<>();

    public VacationProductCategoryFactory() {
        // 初始化策略映射
        strategyMap.put("originA", new OriginAStrategy());
        strategyMap.put("originB", new OriginBStrategy());
    }

    public TempOrderImportStrategy getImportStrategy(String origin) {
        return strategyMap.get(origin);
    }
}
  1. 动态选择与执行:在导入Excel时,控制器通过策略工厂获取对应的策略对象,并调用其 importExcel 方法执行导入逻辑。
@PostMapping("/import")
@Operation(summary = "导入度假订单信息")
public CommonResult<RouteImportRespVo> importExcel(@RequestParam("file") MultipartFile file,
                                                   @RequestParam("origin") String origin) throws Exception {
    // 通过工厂获取对应的策略
    TempOrderImportStrategy importStrategy = (TempOrderImportStrategy) vacationProductCategoryFactory.getImportStrategy(origin);
    if (importStrategy == null) {
        return error(VACATION_ORDER_CONTENT_NOT_EXISTS);
    }

    // 执行导入逻辑
    RouteImportRespVo orderImportRespVo = importStrategy.importExcel(file, origin);
    return success(orderImportRespVo);
}

通过这种方式,策略模式实现了根据不同的来源动态选择导入策略,提高了代码的扩展性和复用性。

RouteOrderInfoDO


package cn.iocoder.central.module.vacation.dal.dataobject.order;


import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
        import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 线路订单信息 DO
 *
 * @author 陕文旅
 */
@TableName("order_route_order_info")
@KeySequence("order_route_order_info_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RouteOrderInfoDO extends BaseDO {

    /**
     * 主键
     */
    @TableId
    private Long id;

    /**
     * 渠道订单号
     */
    private String systemCode;


    /**
     * 订单状态
     *
     * 枚举 {@link TODO order_module_order_status 对应的类}
     */
    private Integer orderStatus;

    /**
     * 订单总金额
     */
    private BigDecimal totalAmount;

    /**
     * 支付方式
     *
     * 枚举 {@link TODO order_module_payment_method 对应的类}
     */
    private Integer paymentMethod;

    /**
     * 支付状态
     *
     * 枚举 {@link TODO order_module_payment_status 对应的类}
     */
    private Integer paymentStatus;

    /**
     * 路线ID
     */
    private Long routeId;

    /**
     * 路线名称
     */
    private String routeName;

    /**
     * 游玩人数
     */
    private Integer playPersonNumber;

    /**
     * 下单时间
     */
    private LocalDateTime buyTime;

    /**
     * 游玩开始时间
     */
    private LocalDateTime playStartTime;

    /**
     * 游玩结束时间
     */
    private LocalDateTime playEndTime;

    /**
     * 取票人/收货人/联系人
     */
    private String orderUserName;

    /**
     * 手机号
     */
    private String orderUserPhone;

    /**
     * 创建者
     */
    private String creator;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新者
     */
    private String updater;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 是否删除
     */
    private Boolean deleted;

    /**
     * 租户编号
     */
    private Long tenantId;
    /*
     * 来源渠道
     * */
    private String origin;
}

RouteOrderInfoDO 是一个典型的 DO(Data Object) 类,它代表数据库中的一个表结构,用于存储和操作数据库中的数据。以下是 DO 的作用和代码中相关注解的解释:

1. DO 的作用

  • 数据映射DO 是数据库表的 Java 对象表示,用于将数据库表中的字段映射到 Java 类的属性。
  • 持久化操作DO 是持久层(DAO 层)与数据库交互的核心对象,用于执行增删改查等操作。
  • 数据封装DO 封装了数据库表的字段,提供了对字段的访问和操作方法。

2. 代码中的注解

以下是代码中使用的注解及其作用:

@TableName
  • 作用:指定该 DO 类对应的数据库表名。
  • 示例
@TableName("order_route_order_info")

表示该类映射到数据库中的 order_route_order_info 表。

@TableId
  • 作用:标注类中的某个字段为主键字段。
  • 示例
@TableId
private Long id;

表示 id 字段是主键。

@KeySequence
  • 作用:用于指定主键生成策略,适用于 Oracle、PostgreSQL 等数据库。
  • 示例
@KeySequence("order_route_order_info_seq")

表示主键生成策略使用 order_route_order_info_seq 序列。

@Data
  • 作用:Lombok 提供的注解,自动生成 getter 和 setter 方法。
  • 效果:减少样板代码,提高开发效率。
@EqualsAndHashCode
  • 作用:Lombok 提供的注解,自动生成 equalshashCode 方法。
  • 参数
    • callSuper = true:表示继承父类的 equalshashCode 方法。
@ToString
  • 作用:Lombok 提供的注解,自动生成 toString 方法。
  • 参数
    • callSuper = true:表示继承父类的 toString 方法。
@Builder
  • 作用:Lombok 提供的注解,生成构建器模式的代码,简化对象的创建。
  • 效果:支持链式调用创建对象。
@NoArgsConstructor 和 @AllArgsConstructor
  • 作用
    • @NoArgsConstructor:生成无参构造函数。
    • @AllArgsConstructor:生成全参构造函数。
  • 效果:减少样板代码,方便对象的创建。

3. 示例字段

以下是 RouteOrderInfoDO 中的一些字段及其作用:

字段名

作用描述

id

主键,唯一标识一个订单。

systemCode

渠道订单号,用于标识订单的来源。

orderStatus

订单状态,枚举值表示订单的不同状态(如待支付、已支付、已完成等)。

totalAmount

订单总金额,表示订单的总费用。

paymentMethod

支付方式,枚举值表示不同的支付方式(如支付宝、微信支付等)。

paymentStatus

支付状态,枚举值表示支付的不同状态(如未支付、已支付、支付失败等)。

routeIdrouteName

路线 ID 和名称,表示订单对应的旅游路线。

playPersonNumber

游玩人数,表示订单中参与游玩的人数。

buyTime

下单时间,表示用户下单的时间。

playStartTimeplayEndTime

游玩开始和结束时间,表示订单的游玩时间段。

orderUserNameorderUserPhone

取票人/收货人信息,包括姓名和手机号。

creatorupdater

创建者和更新者,记录操作人员信息。

createTimeupdateTime

创建时间和更新时间,记录订单的创建和更新时间。

deleted

是否删除,用于软删除机制。

tenantId

租户编号,用于多租户系统。

origin

来源渠道,表示订单的来源(如线上、线下等)。

4. 使用场景

  • 查询数据:通过 DO 类从数据库中查询数据。
RouteOrderInfoDO order = routeOrderInfoMapper.selectById(id);
  • 插入数据:通过 DO 类将数据插入数据库。
RouteOrderInfoDO order = new RouteOrderInfoDO();
order.setSystemCode("order123");
order.setTotalAmount(new BigDecimal("100.00"));
routeOrderInfoMapper.insert(order);
  • 更新数据:通过 DO 类更新数据库中的数据。
RouteOrderInfoDO order = new RouteOrderInfoDO();
order.setId(1L);
order.setOrderStatus(2); // 更新订单状态
routeOrderInfoMapper.updateById(order);

5. 总结

DO 是数据库表的 Java 对象表示,通过注解和 Lombok 工具简化了代码的编写和维护。它在持久层中起到了核心作用,用于数据库的增删改查操作。

RouteOrderInfoMapper

package cn.iocoder.central.module.vacation.dal.mysql.order;

import cn.iocoder.central.module.vacation.controller.admin.order.vo.RouteOrderInfoPageReqVO;
import cn.iocoder.central.module.vacation.controller.admin.order.vo.RouteOrderInfoRespVO;
import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportVo;
import cn.iocoder.central.module.vacation.dal.dataobject.order.RouteOrderInfoDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 子类线路订单信息 Mapper
 *
 * @author 陕文旅
 */
@Mapper
public interface RouteOrderInfoMapper extends BaseMapperX<RouteOrderInfoDO> {

    /**
     * 分页查询订单信息
     *
     * @param reqVO 分页查询请求参数
     * @return 分页结果
     */
    default PageResult<RouteOrderInfoDO> selectPage(RouteOrderInfoPageReqVO reqVO) {
        // 使用 LambdaQueryWrapperX 构建查询条件
        return selectPage(reqVO, new LambdaQueryWrapperX<RouteOrderInfoDO>()
                // 如果 reqVO 中的 routeName 不为空,则模糊匹配 routeName
                .likeIfPresent(RouteOrderInfoDO::getRouteName, reqVO.getRouteName())
                // 按 id 降序排序
                .orderByDesc(RouteOrderInfoDO::getId));
    }

    /**
     * 使用 XML 配置分页查询订单信息
     *
     * @param page 分页对象
     * @param pageReqVO 分页查询请求参数
     * @return 分页结果
     */
    IPage<RouteOrderInfoRespVO> selectPageUseXML(IPage<RouteOrderInfoRespVO> page, @Param("reqVO") RouteOrderInfoPageReqVO pageReqVO);

    /**
     * 获取订单信息列表
     *
     * @param pageReqVO 查询请求参数
     * @return 订单信息列表
     */
    List<RouteImportVo> getRouteOrderInfoList(@Param("reqVO") RouteOrderInfoPageReqVO pageReqVO);

    /**
     * 通过订单编号查询订单信息
     *
     * @param systemCode 订单编号
     * @return 订单信息
     */
    RouteOrderInfoDO selectBySystemCode(String systemCode);

    /**
     * 根据订单编号列表批量查询订单信息
     *
     * @param orderNos 订单编号列表
     * @return 订单信息列表
     */
    List<RouteOrderInfoDO> selectByOrderNos(@Param("orderNos") List<String> orderNos);

    /**
     * 批量插入订单信息
     *
     * @param list 订单信息列表
     * @return 影响的行数
     */
    int batchInsert(@Param("list") List<RouteOrderInfoDO> list);

    /**
     * 批量更新订单信息
     *
     * @param list 订单信息列表
     * @return 影响的行数
     */
    int batchUpdate(@Param("list") List<RouteOrderInfoDO> list);

    /**
     * 根据订单编号和来源渠道查询主键 ID
     *
     * @param systemCode 订单编号
     * @param origin 来源渠道
     * @return 主键 ID
     */
    static Long selectIdBySystemCodeAndOrigin(
            @Param("systemCode") String systemCode,
            @Param("origin") String origin) {
        // 这是一个静态方法,实际实现可能在其他地方
        return null;
    }
}

TempOrderImportStrategy

package cn.iocoder.central.module.vacation.service.order.excel.strategy;



import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportRespVo;
import cn.iocoder.central.module.vacation.dal.dataobject.order.RouteOrderInfoDO;
import cn.iocoder.central.module.vacation.dal.mysql.order.RouteOrderInfoMapper;
import cn.iocoder.yudao.framework.common.util.date.DateTimeFormatterUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

@Component
@Slf4j
public class TempOrderImportStrategy implements VacationOrderImportStrategy {

    @Resource
    private RouteOrderInfoMapper routeOrderInfoMapper;


    @Override
    @Transactional(rollbackFor = Exception.class)  // 事务注解,任何异常都会回滚
    // 修改返回类型以匹配 VacationOrderImportStrategy 接口中的方法签名
    public RouteImportRespVo importExcel(MultipartFile file, String origin) throws IOException {
        // 1. 读取Excel文件内容
        List<RouteOrderInfoDO> orderList = ExcelUtils.read(file, RouteOrderInfoDO.class);

        // 2. 初始化数据容器
        List<RouteOrderInfoDO> createRouteOrderList = new ArrayList<>();  // 待新增订单列表
        List<RouteOrderInfoDO> updateRouteOrderList = new ArrayList<>();  // 待更新订单列表
        List<String> addOrderCodes = new ArrayList<>();  // 新增订单编号集合
        List<String> updateOrderCodes = new ArrayList<>();  // 更新订单编号集合

        // 3. 处理每条订单数据
        for (RouteOrderInfoDO order : orderList) {
            // 3.1 转换VO为DO对象
            RouteOrderInfoDO RouteOrderInfoDO = BeanUtils.toBean(order, RouteOrderInfoDO.class);
            RouteOrderInfoDO.setOrigin(origin);  // 设置订单来源

            // 3.2 格式化时间字段
            RouteOrderInfoDO.setBuyTime(DateTimeFormatterUtils.parseToLocalDateTime(String.valueOf(order.getBuyTime())));
            RouteOrderInfoDO.setPlayStartTime(DateTimeFormatterUtils.parseToLocalDateTime(String.valueOf(order.getPlayStartTime())));
            RouteOrderInfoDO.setPlayEndTime(DateTimeFormatterUtils.parseToLocalDateTime(String.valueOf(order.getPlayEndTime())));

            // 3.3 检查订单是否已存在
            Long tableId = RouteOrderInfoMapper.selectIdBySystemCodeAndOrigin(
                    RouteOrderInfoDO.getSystemCode(),
                    RouteOrderInfoDO.getOrigin()
            );

            if (tableId != null) {
                // 3.4 已存在订单 - 准备更新
                RouteOrderInfoDO.setId(tableId);
                updateRouteOrderList.add(RouteOrderInfoDO);
                updateOrderCodes.add(RouteOrderInfoDO.getSystemCode());
            } else {
                // 3.5 新订单 - 准备新增
                createRouteOrderList.add(RouteOrderInfoDO);
                addOrderCodes.add(RouteOrderInfoDO.getSystemCode());
            }
        }

        // 4. 批量数据库操作
        if (!createRouteOrderList.isEmpty()) {
            routeOrderInfoMapper.insertBatch(createRouteOrderList);// 批量插入新订单
        }
        if (!updateRouteOrderList.isEmpty()) {
            routeOrderInfoMapper.updateBatch(updateRouteOrderList);  // 批量更新已有订单
        }


       RouteImportRespVo respVo = new RouteImportRespVo();
        respVo.setAddOrderCodes(addOrderCodes);  // 设置新增订单编号列表
        respVo.setUpdateOrderCodes(updateOrderCodes);  // 设置更新订单编号列表
        respVo.setFailureOrderCodes(Collections.emptyList());  // 空列表表示没有失败订单

        return respVo;
    }
}

以下是详细的代码注释和设计思路讲解:

设计思路

  1. 策略模式
    • 使用策略模式实现导入逻辑的解耦,使得不同的导入逻辑可以独立实现和扩展。
    • TempOrderImportStrategy 是一个具体的策略类,实现了 VacationOrderImportStrategy 接口。
  1. 事务管理
    • 使用 @Transactional 注解确保导入操作的原子性,任何异常都会导致事务回滚,保证数据一致性。
  1. 批量操作
    • 批量插入和更新订单信息,提高数据库操作的效率。
  1. 数据处理
    • 读取Excel文件内容,转换为 RouteOrderInfoDO 对象。
    • 格式化时间字段,确保数据格式正确。
    • 检查订单是否已存在,决定是新增还是更新。
  1. 结果封装
    • 将导入结果封装为 RouteImportRespVo,返回新增和更新的订单编号列表。

详细代码注释

package cn.iocoder.central.module.vacation.service.order.excel.strategy;

import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportRespVo;
import cn.iocoder.central.module.vacation.dal.dataobject.order.RouteOrderInfoDO;
import cn.iocoder.central.module.vacation.dal.mysql.order.RouteOrderInfoMapper;
import cn.iocoder.yudao.framework.common.util.date.DateTimeFormatterUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * 临时订单导入策略类
 *
 * @author 陕文旅
 */
@Component
@Slf4j
public class TempOrderImportStrategy implements VacationOrderImportStrategy {

    @Resource
    private RouteOrderInfoMapper routeOrderInfoMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)  // 事务注解,任何异常都会回滚
    public RouteImportRespVo importExcel(MultipartFile file, String origin) throws IOException {
        // 1. 读取Excel文件内容
        List<RouteOrderInfoDO> orderList = ExcelUtils.read(file, RouteOrderInfoDO.class);

        // 2. 初始化数据容器
        List<RouteOrderInfoDO> createRouteOrderList = new ArrayList<>();  // 待新增订单列表
        List<RouteOrderInfoDO> updateRouteOrderList = new ArrayList<>();  // 待更新订单列表
        List<String> addOrderCodes = new ArrayList<>();  // 新增订单编号集合
        List<String> updateOrderCodes = new ArrayList<>();  // 更新订单编号集合

        // 3. 处理每条订单数据
        for (RouteOrderInfoDO order : orderList) {
            // 3.1 转换VO为DO对象
            RouteOrderInfoDO RouteOrderInfoDO = BeanUtils.toBean(order, RouteOrderInfoDO.class);
            RouteOrderInfoDO.setOrigin(origin);  // 设置订单来源

            // 3.2 格式化时间字段
            RouteOrderInfoDO.setBuyTime(DateTimeFormatterUtils.parseToLocalDateTime(String.valueOf(order.getBuyTime())));
            RouteOrderInfoDO.setPlayStartTime(DateTimeFormatterUtils.parseToLocalDateTime(String.valueOf(order.getPlayStartTime())));
            RouteOrderInfoDO.setPlayEndTime(DateTimeFormatterUtils.parseToLocalDateTime(String.valueOf(order.getPlayEndTime())));

            // 3.3 检查订单是否已存在
            Long tableId = routeOrderInfoMapper.selectIdBySystemCodeAndOrigin(
                    RouteOrderInfoDO.getSystemCode(),
                    RouteOrderInfoDO.getOrigin()
            );

            if (tableId != null) {
                // 3.4 已存在订单 - 准备更新
                RouteOrderInfoDO.setId(tableId);
                updateRouteOrderList.add(RouteOrderInfoDO);
                updateOrderCodes.add(RouteOrderInfoDO.getSystemCode());
            } else {
                // 3.5 新订单 - 准备新增
                createRouteOrderList.add(RouteOrderInfoDO);
                addOrderCodes.add(RouteOrderInfoDO.getSystemCode());
            }
        }

        // 4. 批量数据库操作
        if (!createRouteOrderList.isEmpty()) {
            routeOrderInfoMapper.insertBatch(createRouteOrderList);  // 批量插入新订单
        }
        if (!updateRouteOrderList.isEmpty()) {
            routeOrderInfoMapper.updateBatch(updateRouteOrderList);  // 批量更新已有订单
        }

        // 5. 构建响应对象
        RouteImportRespVo respVo = new RouteImportRespVo();
        respVo.setAddOrderCodes(addOrderCodes);  // 设置新增订单编号列表
        respVo.setUpdateOrderCodes(updateOrderCodes);  // 设置更新订单编号列表
        respVo.setFailureOrderCodes(Collections.emptyList());  // 空列表表示没有失败订单

        return respVo;
    }
}

代码注释说明

  1. 类注释
    • @Component:将类标记为 Spring 管理的组件。
    • @Slf4j:Lombok 提供的注解,自动生成日志记录器。
  1. 方法注释
    • @Transactional:确保方法在事务中执行,任何异常都会导致事务回滚。
    • @Override:表示该方法重写了父类或接口中的方法。
  1. 字段注释
    • @Resource:注入 RouteOrderInfoMapper,用于与数据库交互。
  1. 代码逻辑注释
    • 读取Excel文件:使用 ExcelUtils.read 方法读取Excel文件内容,并转换为 RouteOrderInfoDO 对象列表。
    • 初始化数据容器:分别初始化待新增和待更新的订单列表,以及新增和更新的订单编号集合。
    • 处理每条订单数据
      • 转换VO为DO对象。
      • 格式化时间字段。
      • 检查订单是否已存在,决定是新增还是更新。
    • 批量数据库操作:批量插入新订单和批量更新已有订单。
    • 构建响应对象:将导入结果封装为 RouteImportRespVo,返回新增和更新的订单编号列表。

总结

该类通过策略模式实现了导入Excel文件的逻辑,使用事务管理确保数据一致性,通过批量操作提高效率,并通过详细的结果封装提供了清晰的导入结果。

VacationOrderImportStrategy

package cn.iocoder.central.module.vacation.service.order.excel.strategy;


import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportRespVo;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

public interface VacationOrderImportStrategy {

    /**
     * 导入Excel
     *
     * @param file
     * @param origin
     * @return
     */
    RouteImportRespVo importExcel(MultipartFile file, String origin) throws IOException;

}

以下是 VacationOrderImportStrategy 接口的详细讲解,包括设计思路和代码注释。

设计思路

  1. 策略模式
    • VacationOrderImportStrategy 是一个策略接口,用于定义导入Excel文件的通用方法。
    • 通过策略模式,可以将不同的导入逻辑封装到不同的实现类中,便于扩展和维护。
  1. 统一接口
    • 定义一个统一的 importExcel 方法,确保所有导入策略都遵循相同的接口。
    • 方法接收Excel文件和来源标识,返回导入结果。
  1. 异常处理
    • 方法声明抛出 IOException,确保在读取文件时的异常能够被正确处理。
  1. 扩展性
    • 通过实现该接口,可以轻松添加新的导入策略,而无需修改现有代码。

总结

VacationOrderImportStrategy 接口通过定义统一的导入方法,确保了导入逻辑的解耦和扩展性。通过实现该接口,可以轻松添加新的导入策略,而无需修改现有代码。这种设计符合开闭原则,提高了系统的可维护性和可扩展性。

TempVacationOrderVo

package cn.iocoder.central.module.vacation.service.order.excel.vo;

import cn.iocoder.central.module.vacation.enums.DictTypeConstants;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.math.BigDecimal;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题
public class TempVacationOrderVo {

    @Schema(description = "导入渠道订单号", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("订单编号")
    private String systemCode;

    @Schema(description = "订单状态(字典)", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty(value = "订单状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.ORDER_MODULE_ORDER_STATUS)
    private String orderStatus;

    @Schema(description = "产品名称", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("产品名称")
    private String routeName;

    @Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("下单时间")
    private String buyTime;

    @Schema(description = "游玩人数", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("游玩人数")
    private Integer playPersonNumber;

    @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("开始时间")
    private String playStartTime;

    @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("结束时间")
    private String playEndTime;

    @Schema(description = "金额", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("金额")
    private BigDecimal totalAmount;

    @Schema(description = "支付方式(字典)", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty(value = "支付方式", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.ORDER_MODULE_PAYMENT_METHOD)
    private String paymentMethod;

    @Schema(description = "支付状态(字典)", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty(value = "支付状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.ORDER_MODULE_PAYMENT_STATUS)
    private String paymentStatus;

    @Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("联系人")
    private String orderUserName;

    @Schema(description = "联系方式", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("联系方式")
    private String orderUserPhone;
}

以下是 TempVacationOrderVo 类的详细讲解,包括设计思路和代码注释。

设计思路

  1. 数据传输对象(VO)
    • TempVacationOrderVo 是一个数据传输对象(Value Object),用于在导入Excel文件时映射Excel中的数据到Java对象。
    • 它封装了订单信息的各个字段,并提供了相应的注解来支持数据的导入和导出。
  1. 注解支持
    • 使用 @Schema 注解描述字段的含义,便于生成API文档。
    • 使用 @ExcelProperty 注解指定Excel文件中的列名,支持Excel导入导出。
    • 使用 @DictFormat 注解指定字段的字典格式,支持字典转换。
  1. Lombok支持
    • 使用 Lombok 的注解(如 @Data@Builder 等)简化代码,减少样板代码的编写。
  1. 字典转换
    • 使用 @DictFormatDictConvert 支持字段的字典转换,确保数据的一致性和可读性。

TempVacationOrderVo 类通过注解支持实现了Excel导入导出的功能,并通过 Lombok 注解简化了代码的编写。它封装了订单信息的各个字段,并提供了字典转换的支持,确保数据的一致性和可读性。这种设计使得代码更加简洁和易于维护。

RouteOrderInfoServiceImpl

package cn.iocoder.central.module.vacation.service.order;

import cn.hutool.core.collection.CollectionUtil;
import cn.iocoder.central.module.vacation.controller.admin.order.vo.RouteOrderInfoPageReqVO;
import cn.iocoder.central.module.vacation.controller.admin.order.vo.RouteOrderInfoRespVO;
import cn.iocoder.central.module.vacation.controller.admin.order.vo.RouteOrderInfoSaveReqVO;
import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportRespVo;
import cn.iocoder.central.module.vacation.controller.admin.order.excel.vo.RouteImportVo;
import cn.iocoder.central.module.vacation.dal.dataobject.order.RouteOrderInfoDO;
import cn.iocoder.central.module.vacation.dal.mysql.order.RouteOrderInfoMapper;
import cn.iocoder.central.module.vacation.utils.DateTimeFormatterUtils;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

import java.util.ArrayList;
import java.util.List;

import static cn.iocoder.central.module.vacation.enums.ErrorCodeConstants.CHILD_ROUTE_ORDER_INFO_NOT_EXISTS;
import static cn.iocoder.central.module.vacation.enums.ErrorCodeConstants.ROUTE_IMPORT_LIST_IS_EMPTY;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;

/**
 * 子类线路订单信息 Service 实现类
 * 提供订单的增删改查、分页查询、批量导入导出等核心功能
 *
 * @author 陕文旅
 */
@Service // 标识为Spring服务层Bean
@Validated // 启用方法参数校验功能
@Slf4j // 自动生成日志对象
public class RouteOrderInfoServiceImpl implements RouteOrderInfoService {

    @Resource // 注入MyBatis Mapper接口
    private RouteOrderInfoMapper routeOrderInfoMapper;

    /**
     * 创建子类线路订单
     * @param createReqVO 订单创建请求数据
     * @return 返回新创建的订单ID
     */
    @Override
    public Long createChildRouteOrderInfo(RouteOrderInfoSaveReqVO createReqVO) {
        // 将VO转换为数据库实体对象
        RouteOrderInfoDO childRouteOrderInfo = BeanUtils.toBean(createReqVO, RouteOrderInfoDO.class);
        // 执行数据库插入操作
        routeOrderInfoMapper.insert(childRouteOrderInfo);
        // 返回新生成的主键ID
        return childRouteOrderInfo.getId();
    }

    /**
     * 更新子类线路订单
     * @param updateReqVO 订单更新请求数据(需包含ID)
     */
    @Override
    public void updateChildRouteOrderInfo(RouteOrderInfoSaveReqVO updateReqVO) {
        // 校验订单是否存在(不存在则抛出异常)
        validateChildRouteOrderInfoExists(updateReqVO.getId());
        // 转换VO为DO并更新
        RouteOrderInfoDO updateObj = BeanUtils.toBean(updateReqVO, RouteOrderInfoDO.class);
        routeOrderInfoMapper.updateById(updateObj);
    }

    /**
     * 删除子类线路订单
     * @param id 要删除的订单ID
     */
    @Override
    public void deleteChildRouteOrderInfo(Long id) {
        // 校验订单是否存在
        validateChildRouteOrderInfoExists(id);
        // 执行删除操作
        routeOrderInfoMapper.deleteById(id);
    }

    /**
     * 校验订单是否存在
     * @param id 订单ID
     * @throws cn.iocoder.yudao.framework.common.exception.ServiceException 当订单不存在时抛出
     */
    private void validateChildRouteOrderInfoExists(Long id) {
        if (routeOrderInfoMapper.selectById(id) == null) {
            // 使用统一异常处理抛出业务异常
            throw exception(CHILD_ROUTE_ORDER_INFO_NOT_EXISTS);
        }
    }

    /**
     * 根据ID获取订单详情
     * @param id 订单ID
     * @return 订单数据对象(可能为null)
     */
    @Override
    public RouteOrderInfoDO getRouteOrderInfo(Long id) {
        return routeOrderInfoMapper.selectById(id);
    }

    /**
     * 分页查询订单列表
     * @param pageReqVO 分页查询条件
     * @return 分页结果(包含数据和总数)
     */
    @Override
    public PageResult<RouteOrderInfoRespVO> getRouteOrderInfoPage(RouteOrderInfoPageReqVO pageReqVO) {
        // 创建MyBatis Plus分页对象
        IPage<RouteOrderInfoRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
        // 调用自定义XML分页查询
        routeOrderInfoMapper.selectPageUseXML(page, pageReqVO);
        // 包装返回结果
        return new PageResult<>(page.getRecords(), page.getTotal());
    }

    /**
     * 批量导入订单数据
     * @param list 要导入的订单列表
     * @param updateSupport 是否支持更新已存在订单(当前参数未使用)
     * @return 导入结果(包含成功/失败记录)
     */
    @Override
    @Transactional // 声明式事务管理(原子性操作)
    public RouteImportRespVo importRouteList(List<RouteImportVo> list, Boolean updateSupport) {
        // 参数校验:空列表检查
        if (CollectionUtil.isEmpty(list)) {
            throw exception(ROUTE_IMPORT_LIST_IS_EMPTY);
        }

        // 初始化导入结果对象
        RouteImportRespVo respVO = new RouteImportRespVo();
        respVO.setAddOrderCodes(new ArrayList<>()); // 新增订单编号集合
        respVO.setUpdateOrderCodes(new ArrayList<>()); // 更新订单编号集合
        respVO.setFailureOrderCodes(new ArrayList<>()); // 失败订单编号集合(当前未使用)

        // 记录导入开始日志
        log.info("\n================================== 导入开始 ============================");
        long startImportTime = System.currentTimeMillis();

        // 遍历处理每个导入项
        list.forEach(item -> {
            try {
                log.info("开始导入:{}", item);
                
                // 数据预处理:格式化时间字段
                // 说明:将字符串类型的时间转换为数据库兼容的格式
                item.setBuyTime(DateTimeFormatterUtils.formatDateTime(item.getBuyTime()));
                item.setPlayStartTime(DateTimeFormatterUtils.formatDateTime(item.getPlayStartTime()));
                item.setPlayEndTime(DateTimeFormatterUtils.formatDateTime(item.getPlayEndTime()));

                // 检查订单唯一性(根据系统编码)
                RouteOrderInfoDO existingOrder = routeOrderInfoMapper.selectBySystemCode(item.getSystemCode());

                if (existingOrder != null) {
                    // 更新已存在订单
                    RouteOrderInfoSaveReqVO updateReqVO = BeanUtils.toBean(item, RouteOrderInfoSaveReqVO.class);
                    updateReqVO.setId(existingOrder.getId()); // 设置需要更新的订单ID
                    updateChildRouteOrderInfo(updateReqVO);
                    respVO.getUpdateOrderCodes().add(item.getSystemCode());
                    log.info("更新订单:{}", item.getSystemCode());
                } else {
                    // 创建新订单
                    RouteOrderInfoSaveReqVO createReqVO = BeanUtils.toBean(item, RouteOrderInfoSaveReqVO.class);
                    Long orderId = createChildRouteOrderInfo(createReqVO);
                    respVO.getAddOrderCodes().add(item.getSystemCode());
                    log.info("创建订单:{}", item.getSystemCode());
                }
            } catch (Exception e) {
                // 异常处理(当前简单记录日志,实际可扩展记录失败原因)
                log.error("导入订单失败:{},错误原因:{}", item.getSystemCode(), e.getMessage());
                respVO.getFailureOrderCodes().add(item.getSystemCode());
            }
        });

        // 记录导入耗时
        long endImportTime = System.currentTimeMillis();
        log.info("\n================================== 导入结束 ============================");
        log.info("总耗时:{} 毫秒", endImportTime - startImportTime);

        return respVO;
    }

    /**
     * 获取订单列表(用于数据导出)
     * @param pageReqVO 查询条件(实际应使用专用导出参数)
     * @return 订单数据列表(VO格式)
     */
    @Override
    public List<RouteImportVo> getRouteOrderInfoList(RouteOrderInfoPageReqVO pageReqVO) {
        // 调用Mapper获取原始数据并自动转换为VO
        return routeOrderInfoMapper.getRouteOrderInfoList(pageReqVO);
    }
}

该代码是用于管理子类线路订单信息的服务实现类,以下是其设计解析:


1. 类结构与依赖

  • 注解
    • @Service:标识为Spring管理的服务类。
    • @Validated:启用方法参数校验。
    • @Slf4j:集成Lombok日志工具。
  • 依赖
    • 通过@Resource注入RouteOrderInfoMapper,操作数据库的MyBatis Mapper接口。

2. 核心方法解析

(1) 增删改查基础操作
  • createChildRouteOrderInfo
    • 将请求VO转换为DO对象并插入数据库,返回生成的主键ID。
    • 注意:依赖BeanUtils.toBean进行对象转换,需确保VO与DO字段匹配。
  • updateChildRouteOrderInfo
    • 先校验订单是否存在(validateChildRouteOrderInfoExists),再更新数据。
    • 校验逻辑:通过ID查询记录,不存在则抛异常CHILD_ROUTE_ORDER_INFO_NOT_EXISTS
  • deleteChildRouteOrderInfo
    • 类似更新操作,先校验存在性再删除。
  • getRouteOrderInfo
    • 直接通过ID查询单个订单,返回DO对象。
(2) 分页查询
  • getRouteOrderInfoPage
    • 使用MyBatis Plus的Page对象封装分页参数。
    • 调用Mapper的selectPageUseXML方法,通过XML自定义SQL分页查询。
    • 关键点:需确保XML中的SQL正确实现分页逻辑。
(3) 订单导入功能
  • importRouteList
    • 事务控制:通过@Transactional保证原子性。
    • 流程
      1. 参数校验:导入列表为空则抛异常ROUTE_IMPORT_LIST_IS_EMPTY
      2. 初始化响应对象:记录新增、更新及失败的订单号。
      3. 遍历处理每个订单
      • 时间格式化:处理buyTimeplayStartTime等字段。
      • 唯一性校验:通过systemCode查询是否存在。
      • 更新或插入:存在则更新,否则新增。
    • 日志监控:记录导入耗时,便于性能分析。
    • 潜在问题
      • systemCode非唯一键,可能导致数据重复。
      • 异常处理不足,个别订单失败会导致整个事务回滚。
(4) 数据导出
  • getRouteOrderInfoList
    • 调用Mapper获取订单列表,用于导出数据(如Excel)。
    • 需确保查询结果与RouteImportVo字段映射正确。

3. 设计亮点

  • 分层清晰:通过VO、DO隔离不同层的数据结构,符合MVC规范。
  • 复用性:使用BeanUtils简化对象转换,减少冗余代码。
  • 事务管理:导入功能通过@Transactional保证数据一致性。
  • 日志跟踪:详细记录导入过程,便于排查问题。

4. 潜在问题与优化建议

  • 并发问题
    • 场景:并发导入相同systemCode的订单时,可能重复插入。
    • 建议:在数据库为systemCode添加唯一索引,或使用分布式锁。
  • 异常处理
    • 问题:单个订单失败会导致整个导入回滚。
    • 优化:捕获单条记录的异常,记录失败原因,继续处理后续数据。
  • 时间格式化风险
    • 问题:若输入时间格式不符,会抛出未处理的异常。
    • 改进:增加try-catch块,将错误订单加入failureOrderCodes
  • 性能瓶颈
    • 场景:大数据量导入时逐条处理效率低。
    • 优化:改用批量插入/更新操作,提升性能。
  • 唯一性校验优化
    • 现状:单条查询selectBySystemCode可能效率低。
    • 改进:预先批量查询所有systemCode,减少数据库交互次数。

5. 总结

该服务实现类较好地完成了订单管理的核心功能,但在高并发、大数据量及异常处理方面存在改进空间。通过增加数据库约束、优化批处理逻辑和细化异常捕获,可进一步提升健壮性和性能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时雨h

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值