以ERP某功能开发为例:梳理从需求分析到单元测试的全流程

289 阅读11分钟

ERP系统功能开发全流程:从需求分析到单元测试(含分页逻辑)

本文以ERP系统中销售订单管理模块的开发为例,详细讲解从需求分析、数据库设计、代码实现到单元测试的全流程。我们将模拟产品经理的需求,基于MyBatis-Plus设计实体类、VO、DTO,定义ApiFox接口规范,使用ApiResponse封装返回结果,添加分页查询逻辑,并使用Mockito编写专业的单元测试。


一、业务需求分析

1.1 需求背景

作为一个ERP系统,销售订单管理模块需要支持以下核心功能:

  1. 订单创建:销售人员创建销售订单,包含客户信息、商品明细、订单总额等。
  2. 订单审批:订单需经过多级审批(如部门主管、财务)。
  3. 订单状态管理:支持订单状态流转(草稿、待审批、已审批、已发货、已完成、已取消)。
  4. 库存校验:创建订单时需校验商品库存是否足够。
  5. 客户信用检查:订单创建前需检查客户信用额度是否足够。
  6. 订单分页查询:支持按客户ID或订单状态分页查询订单列表。

1.2 功能需求

  • 订单创建

    • 输入:客户ID、商品列表(商品ID、数量)、订单备注。
    • 输出:订单ID、订单状态。
    • 约束:客户信用额度需满足订单总额;商品库存需足够。
  • 订单审批

    • 输入:订单ID、审批人ID、审批结果(通过/拒绝)、审批备注。
    • 输出:更新后的订单状态。
  • 订单查询

    • 输入:订单ID(详情查询)或客户ID/状态(分页查询)。
    • 输出:订单详情(包括商品明细、审批记录)或分页订单列表。

1.3 非功能需求

  • 高并发:支持多人同时创建和审批订单。
  • 数据一致性:订单状态、库存、信用额度需保持一致。
  • 可扩展性:支持后续添加新功能,如折扣、促销等。

二、数据库设计

2.1 实体和关联表设计

根据需求,我们需要以下核心实体和关联表:

实体表
  1. 客户表 (customer)

    • 存储客户基本信息和信用额度。
    • 字段:customer_id (主键), name, credit_limit, create_time, update_time
  2. 商品表 (product)

    • 存储商品信息和库存。
    • 字段:product_id (主键), name, price, stock, create_time, update_time
  3. 订单表 (sales_order)

    • 存储订单基本信息。
    • 字段:order_id (主键), customer_id (外键), total_amount, status (草稿/待审批/已审批/已发货/已完成/已取消), remark, create_time, update_time
  4. 审批记录表 (approval_record)

    • 存储订单审批历史。
    • 字段:record_id (主键), order_id (外键), approver_id, result (通过/拒绝), remark, create_time
关联表
  1. 订单商品明细表 (order_item)

    • 存储订单中的商品明细。
    • 字段:item_id (主键), order_id (外键), product_id (外键), quantity, unit_price, subtotal

2.2 表结构SQL

-- 客户表
CREATE TABLE customer (
    customer_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    credit_limit DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 商品表
CREATE TABLE product (
    product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 订单表
CREATE TABLE sales_order (
    order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    customer_id BIGINT NOT NULL,
    total_amount DECIMAL(10, 2) NOT NULL,
    status ENUM('DRAFT', 'PENDING', 'APPROVED', 'SHIPPED', 'COMPLETED', 'CANCELED') NOT NULL,
    remark TEXT,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (customer_id) REFERENCES customer(customer_id)
);

-- 订单商品明细表
CREATE TABLE order_item (
    item_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,
    product_id BIGINT NOT NULL,
    quantity INT NOT NULL,
    unit_price DECIMAL(10, 2) NOT NULL,
    subtotal DECIMAL(10, 2) NOT NULL,
    FOREIGN KEY (order_id) REFERENCES sales_order(order_id),
    FOREIGN KEY (product_id) REFERENCES product(product_id)
);

-- 审批记录表
CREATE TABLE approval_record (
    record_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_id BIGINT NOT NULL,
    approver_id BIGINT NOT NULL,
    result ENUM('APPROVED', 'REJECTED') NOT NULL,
    remark TEXT,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (order_id) REFERENCES sales_order(order_id)
);

2.3 设计说明

  • 订单表:使用ENUM类型限制订单状态,减少非法状态的可能性。
  • 订单商品明细表:存储unit_pricesubtotal,避免价格波动导致计算不一致。
  • 审批记录表:记录每次审批的操作,便于追溯。
  • 外键约束:确保数据一致性,如订单必须关联有效客户。

三、PO、VO、DTO设计

基于MyBatis-Plus,我们设计以下Java类。

3.1 PO类(持久化对象)

Customer.java
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("customer")
public class Customer {
    @TableId(type = IdType.AUTO)
    private Long customerId;
    private String name;
    private BigDecimal creditLimit;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
Product.java
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("product")
public class Product {
    @TableId(type = IdType.AUTO)
    private Long productId;
    private String name;
    private BigDecimal price;
    private Integer stock;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
SalesOrder.java
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@TableName("sales_order")
public class SalesOrder {
    @TableId(type = IdType.AUTO)
    private Long orderId;
    private Long customerId;
    private BigDecimal totalAmount;
    private String status;
    private String remark;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}
OrderItem.java
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;

@Data
@TableName("order_item")
public class OrderItem {
    @TableId(type = IdType.AUTO)
    private Long itemId;
    private Long orderId;
    private Long productId;
    private Integer quantity;
    private BigDecimal unitPrice;
    private BigDecimal subtotal;
}
ApprovalRecord.java
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;

@Data
@TableName("approval_record")
public class ApprovalRecord {
    @TableId(type = IdType.AUTO)
    private Long recordId;
    private Long orderId;
    private Long approverId;
    private String result;
    private String remark;
    private LocalDateTime createTime;
}

3.2 DTO类(数据传输对象)

CreateOrderRequestDTO.java
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;

@Data
public class CreateOrderRequestDTO {
    private Long customerId;
    private String remark;
    private List<OrderItemDTO> items;

    @Data
    public static class OrderItemDTO {
        private Long productId;
        private Integer quantity;
    }
}
ApproveOrderRequestDTO.java
import lombok.Data;

@Data
public class ApproveOrderRequestDTO {
    private Long orderId;
    private Long approverId;
    private String result;
    private String remark;
}
OrderPageRequestDTO.java
import lombok.Data;

@Data
public class OrderPageRequestDTO {
    private Long customerId;
    private String status;
    private Integer pageNum;
    private Integer pageSize;
}

3.3 VO类(视图对象)

OrderDetailVO.java
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

@Data
public class OrderDetailVO {
    private Long orderId;
    private Long customerId;
    private String customerName;
    private BigDecimal totalAmount;
    private String status;
    private String remark;
    private LocalDateTime createTime;
    private List<OrderItemVO> items;
    private List<ApprovalRecordVO> approvalRecords;

    @Data
    public static class OrderItemVO {
        private Long productId;
        private String productName;
        private Integer quantity;
        private BigDecimal unitPrice;
        private BigDecimal subtotal;
    }

    @Data
    public static class ApprovalRecordVO {
        private Long approverId;
        private String result;
        private String remark;
        private LocalDateTime createTime;
    }
}
OrderListVO.java
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class OrderListVO {
    private Long orderId;
    private Long customerId;
    private String customerName;
    private BigDecimal totalAmount;
    private String status;
    private LocalDateTime createTime;
}

3.4 响应封装类

ApiResponse.java
import lombok.Data;

@Data
public class ApiResponse<T> {
    private Integer code;
    private String msg;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(0);
        response.setMsg("成功");
        response.setData(data);
        return response;
    }

    public static <T> ApiResponse<T> error(String msg) {
        ApiResponse<T> response = new ApiResponse<>();
        response.setCode(-1);
        response.setMsg(msg);
        return response;
    }
}

3.5 设计说明

  • PO类:映射数据库表,字段与表一一对应,使用MyBatis-Plus注解简化开发。
  • DTO类:新增OrderPageRequestDTO支持分页查询,包含分页参数和查询条件。
  • VO类:新增OrderListVO用于分页查询返回,简化字段。
  • ApiResponse:统一接口返回格式,包含状态码、消息和数据。

四、ApiFox接口设计

4.1 接口:创建订单

  • URL/api/order/create

  • Method:POST

  • 请求头

    • Authorization: Bearer {token} (测试权限的token)
    • Content-Type: application/json
  • 入参

{
    "customerId": 1,
    "remark": "急单",
    "items": [
        {
            "productId": 1,
            "quantity": 10
        },
        {
            "productId": 2,
            "quantity": 5
        }
    ]
}
  • 出参
{
    "code": 0,
    "msg": "成功",
    "data": {
        "orderId": 1001,
        "status": "DRAFT"
    }
}

4.2 接口:审批订单

  • URL/api/order/approve

  • Method:POST

  • 请求头

    • Authorization: Bearer {token}
    • Content-Type: application/json
  • 入参

{
    "orderId": 1001,
    "approverId": 2,
    "result": "APPROVED",
    "remark": "通过"
}
  • 出参
{
    "code": 0,
    "msg": "成功",
    "data": {
        "orderId": 1001,
        "status": "APPROVED"
    }
}

4.3 接口:查询订单详情

  • URL/api/order/detail/{orderId}

  • Method:GET

  • 请求头

    • Authorization: Bearer {token}
  • 出参

{
    "code": 0,
    "msg": "成功",
    "data": {
        "orderId": 1001,
        "customerId": 1,
        "customerName": "客户A",
        "totalAmount": 1500.00,
        "status": "APPROVED",
        "remark": "急单",
        "createTime": "2025-05-11T10:00:00",
        "items": [
            {
                "productId": 1,
                "productName": "商品A",
                "quantity": 10,
                "unitPrice": 100.00,
                "subtotal": 1000.00
            }
        ],
        "approvalRecords": [
            {
                "approverId": 2,
                "result": "APPROVED",
                "remark": "通过",
                "createTime": "2025-05-11T10:30:00"
            }
        ]
    }
}

4.4 接口:分页查询订单列表

  • URL/api/order/list

  • Method:POST

  • 请求头

    • Authorization: Bearer {token}
    • Content-Type: application/json
  • 入参

{
    "customerId": 1,
    "status": "APPROVED",
    "pageNum": 1,
    "pageSize": 10
}
  • 出参
{
    "code": 0,
    "msg": "成功",
    "data": {
        "total": 100,
        "records": [
            {
                "orderId": 1001,
                "customerId": 1,
                "customerName": "客户A",
                "totalAmount": 1500.00,
                "status": "APPROVED",
                "createTime": "2025-05-11T10:00:00"
            }
        ],
        "current": 1,
        "size": 10
    }
}

五、Service层实现

基于MyBatis-Plus,我们实现订单管理的Service层,添加分页查询逻辑。

5.1 Service接口

OrderService.java
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.erp.dto.CreateOrderRequestDTO;
import com.example.erp.dto.ApproveOrderRequestDTO;
import com.example.erp.dto.OrderPageRequestDTO;
import com.example.erp.entity.SalesOrder;
import com.example.erp.vo.OrderDetailVO;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

public interface OrderService extends IService<SalesOrder> {
    Long createOrder(CreateOrderRequestDTO dto);
    void approveOrder(ApproveOrderRequestDTO dto);
    OrderDetailVO getOrderDetail(Long orderId);
    Page<OrderListVO> pageOrderList(OrderPageRequestDTO dto);
}

5.2 Service实现

OrderServiceImpl.java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.erp.dto.CreateOrderRequestDTO;
import com.example.erp.dto.ApproveOrderRequestDTO;
import com.example.erp.dto.OrderPageRequestDTO;
import com.example.erp.entity.*;
import com.example.erp.mapper.*;
import com.example.erp.vo.OrderDetailVO;
import com.example.erp.vo.OrderListVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, SalesOrder> implements OrderService {

    @Autowired
    private CustomerMapper customerMapper;
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    @Autowired
    private ApprovalRecordMapper approvalRecordMapper;

    @Override
    @Transactional(rollbackOn = Exception.class)
    public Long createOrder(CreateOrderRequestDTO dto) {
        // 1. 校验客户信用额度
        Customer customer = customerMapper.selectById(dto.getCustomerId());
        if (customer == null) {
            throw new RuntimeException("客户不存在");
        }

        // 2. 计算订单总额并校验库存
        BigDecimal totalAmount = BigDecimal.ZERO;
        for (CreateOrderRequestDTO.OrderItemDTO item : dto.getItems()) {
            Product product = productMapper.selectById(item.getProductId());
            if (product == null) {
                throw new RuntimeException("商品不存在");
            }
            if (product.getStock() < item.getQuantity()) {
                throw new RuntimeException("库存不足");
            }
            totalAmount = totalAmount.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
        }

        if (customer.getCreditLimit().compareTo(totalAmount) < 0) {
            throw new RuntimeException("信用额度不足");
        }

        // 3. 创建订单
        SalesOrder order = new SalesOrder();
        order.setCustomerId(dto.getCustomerId());
        order.setTotalAmount(totalAmount);
        order.setStatus("DRAFT");
        order.setRemark(dto.getRemark());
        baseMapper.insert(order);

        // 4. 创建订单明细并更新库存
        for (CreateOrderRequestDTO.OrderItemDTO item : dto.getItems()) {
            Product product = productMapper.selectById(item.getProductId());
            OrderItem orderItem = new OrderItem();
            orderItem.setOrderId(order.getOrderId());
            orderItem.setProductId(item.getProductId());
            orderItem.setQuantity(item.getQuantity());
            orderItem.setUnitPrice(product.getPrice());
            orderItem.setSubtotal(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())));
            orderItemMapper.insert(orderItem);

            // 更新库存
            product.setStock(product.getStock() - item.getQuantity());
            productMapper.updateById(product);
        }

        return order.getOrderId();
    }

    @Override
    @Transactional(rollbackOn = Exception.class)
    public void approveOrder(ApproveOrderRequestDTO dto) {
        SalesOrder order = baseMapper.selectById(dto.getOrderId());
        if (order == null) {
            throw new RuntimeException("订单不存在");
        }
        if (!"PENDING".equals(order.getStatus())) {
            throw new RuntimeException("订单状态不正确");
        }

        // 更新订单状态
        order.setStatus("APPROVED".equals(dto.getResult()) ? "APPROVED" : "CANCELED");
        baseMapper.updateById(order);

        // 记录审批
        ApprovalRecord record = new ApprovalRecord();
        record.setOrderId(dto.getOrderId());
        record.setApproverId(dto.getApproverId());
        record.setResult(dto.getResult());
        record.setRemark(dto.getRemark());
        approvalRecordMapper.insert(record);
    }

    @Override
    public OrderDetailVO getOrderDetail(Long orderId) {
        SalesOrder order = baseMapper.selectById(orderId);
        if (order == null) {
            throw new RuntimeException("订单不存在");
        }

        OrderDetailVO vo = new OrderDetailVO();
        vo.setOrderId(order.getOrderId());
        vo.setCustomerId(order.getCustomerId());
        vo.setTotalAmount(order.getTotalAmount());
        vo.setStatus(order.getStatus());
        vo.setRemark(order.getRemark());
        vo.setCreateTime(order.getCreateTime());

        // 查询客户名称
        Customer customer = customerMapper.selectById(order.getCustomerId());
        vo.setCustomerName(customer != null ? customer.getName() : "");

        // 查询订单明细
        List<OrderItem> items = orderItemMapper.selectList(new QueryWrapper<OrderItem>().eq("order_id", orderId));
        List<OrderDetailVO.OrderItemVO> itemVOs = items.stream().map(item -> {
            OrderDetailVO.OrderItemVO itemVO = new OrderDetailVO.OrderItemVO();
            itemVO.setProductId(item.getProductId());
            itemVO.setQuantity(item.getQuantity());
            itemVO.setUnitPrice(item.getUnitPrice());
            itemVO.setSubtotal(item.getSubtotal());
            Product product = productMapper.selectById(item.getProductId());
            itemVO.setProductName(product != null ? product.getName() : "");
            return itemVO;
        }).collect(Collectors.toList());
        vo.setItems(itemVOs);

        // 查询审批记录
        List<ApprovalRecord> records = approvalRecordMapper.selectList(new QueryWrapper<ApprovalRecord>().eq("order_id", orderId));
        List<OrderDetailVO.ApprovalRecordVO> recordVOs = records.stream().map(record -> {
            OrderDetailVO.ApprovalRecordVO recordVO = new OrderDetailVO.ApprovalRecordVO();
            recordVO.setApproverId(record.getApproverId());
            recordVO.setResult(record.getResult());
            recordVO.setRemark(record.getRemark());
            recordVO.setCreateTime(record.getCreateTime());
            return recordVO;
        }).collect(Collectors.toList());
        vo.setApprovalRecords(recordVOs);

        return vo;
    }

    @Override
    public Page<OrderListVO> pageOrderList(OrderPageRequestDTO dto) {
        // 构建分页对象
        Page<SalesOrder> page = new Page<>(dto.getPageNum(), dto.getPageSize());

        // 构建查询条件
        QueryWrapper<SalesOrder> wrapper = new QueryWrapper<>();
        if (dto.getCustomerId() != null) {
            wrapper.eq("customer_id", dto.getCustomerId());
        }
        if (dto.getStatus() != null) {
            wrapper.eq("status", dto.getStatus());
        }
        wrapper.orderByDesc("create_time");

        // 执行分页查询
        Page<SalesOrder> orderPage = baseMapper.selectPage(page, wrapper);

        // 转换为VO
        Page<OrderListVO> result = new Page<>();
        result.setTotal(orderPage.getTotal());
        result.setCurrent(orderPage.getCurrent());
        result.setSize(orderPage.getSize());
        result.setRecords(orderPage.getRecords().stream().map(order -> {
            OrderListVO vo = new OrderListVO();
            vo.setOrderId(order.getOrderId());
            vo.setCustomerId(order.getCustomerId());
            vo.setTotalAmount(order.getTotalAmount());
            vo.setStatus(order.getStatus());
            vo.setCreateTime(order.getCreateTime());
            Customer customer = customerMapper.selectById(order.getCustomerId());
            vo.setCustomerName(customer != null ? customer.getName() : "");
            return vo;
        }).collect(Collectors.toList()));

        return result;
    }
}

5.3 实现说明

  • 分页逻辑:使用MyBatis-Plus的Page对象实现分页,OrderPageRequestDTO接收分页参数和查询条件。
  • 条件查询:通过QueryWrapper动态构建查询条件,支持按客户ID和状态过滤。
  • VO转换:将分页查询结果转换为OrderListVO,包含客户名称等前端所需字段。
  • 事务管理createOrderapproveOrder使用@Transactional注解,确保数据一致性。

六、单元测试

使用Mockito编写单元测试,覆盖核心场景,包括新增的分页查询。

6.1 测试代码

OrderServiceImplTest.java
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.erp.dto.CreateOrderRequestDTO;
import com.example.erp.dto.ApproveOrderRequestDTO;
import com.example.erp.dto.OrderPageRequestDTO;
import com.example.erp.entity.*;
import com.example.erp.mapper.*;
import com.example.erp.service.OrderService;
import com.example.erp.service.impl.OrderServiceImpl;
import com.example.erp.vo.OrderListVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

public class OrderServiceImplTest {

    @InjectMocks
    private OrderServiceImpl orderService;

    @Mock
    private OrderMapper orderMapper;
    @Mock
    private CustomerMapper customerMapper;
    @Mock
    private ProductMapper productMapper;
    @Mock
    private OrderItemMapper orderItemMapper;
    @Mock
    private ApprovalRecordMapper approvalRecordMapper;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    void testCreateOrder_Success() {
        // 准备数据
        CreateOrderRequestDTO dto = new CreateOrderRequestDTO();
        dto.setCustomerId(1L);
        dto.setRemark("急单");
        CreateOrderRequestDTO.OrderItemDTO itemDTO = new CreateOrderRequestDTO.OrderItemDTO();
        itemDTO.setProductId(1L);
        itemDTO.setQuantity(10);
        dto.setItems(Collections.singletonList(itemDTO));

        Customer customer = new Customer();
        customer.setCustomerId(1L);
        customer.setCreditLimit(new BigDecimal("10000.00"));

        Product product = new Product();
        product.setProductId(1L);
        product.setPrice(new BigDecimal("100.00"));
        product.setStock(100);

        SalesOrder order = new SalesOrder();
        order.setOrderId(1001L);

        // Mock行为
        when(customerMapper.selectById(1L)).thenReturn(customer);
        when(productMapper.selectById(1L)).thenReturn(product);
        when(orderMapper.insert(any(SalesOrder.class))).thenReturn(1);
        when(orderItemMapper.insert(any(OrderItem.class))).thenReturn(1);
        when(productMapper.updateById(any(Product.class))).thenReturn(1);

        // 执行
        Long orderId = orderService.createOrder(dto);

        // 验证
        assertNotNull(orderId);
        verify(orderMapper, times(1)).insert(any(SalesOrder.class));
        verify(orderItemMapper, times(1)).insert(any(OrderItem.class));
        verify(productMapper, times(1)).updateById(any(Product.class));
    }

    @Test
    void testCreateOrder_InsufficientCredit() {
        // 准备数据
        CreateOrderRequestDTO dto = new CreateOrderRequestDTO();
        dto.setCustomerId(1L);
        CreateOrderRequestDTO.OrderItemDTO itemDTO = new CreateOrderRequestDTO.OrderItemDTO();
        itemDTO.setProductId(1L);
        itemDTO.setQuantity(10);
        dto.setItems(Collections.singletonList(itemDTO));

        Customer customer = new Customer();
        customer.setCustomerId(1L);
        customer.setCreditLimit(new BigDecimal("500.00"));

        Product product = new Product();
        product.setProductId(1L);
        product.setPrice(new BigDecimal("100.00"));
        product.setStock(100);

        // Mock行为
        when(customerMapper.selectById(1L)).thenReturn(customer);
        when(productMapper.selectById(1L)).thenReturn(product);

        // 执行并验证异常
        assertThrows(RuntimeException.class, () -> orderService.createOrder(dto));
        verify(orderMapper, never()).insert(any(SalesOrder.class));
    }

    @Test
    void testApproveOrder_Success() {
        // 准备数据
        ApproveOrderRequestDTO dto = new ApproveOrderRequestDTO();
        dto.setOrderId(1001L);
        dto.setApproverId(2L);
        dto.setResult("APPROVED");
        dto.setRemark("通过");

        SalesOrder order = new SalesOrder();
        order.setOrderId(1001L);
        order.setStatus("PENDING");

        // Mock行为
        when(orderMapper.selectById(1001L)).thenReturn(order);
        when(orderMapper.updateById(any(SalesOrder.class))).thenReturn(1);
        when(approvalRecordMapper.insert(any(ApprovalRecord.class))).thenReturn(1);

        // 执行
        orderService.approveOrder(dto);

        // 验证
        verify(orderMapper, times(1)).updateById(any(SalesOrder.class));
        verify(approvalRecordMapper, times(1)).insert(any(ApprovalRecord.class));
    }

    @Test
    void testGetOrderDetail_Success() {
        // 准备数据
        SalesOrder order = new SalesOrder();
        order.setOrderId(1001L);
        order.setCustomerId(1L);
        order.setTotalAmount(new BigDecimal("1000.00"));
        order.setStatus("APPROVED");

        Customer customer = new Customer();
        customer.setCustomerId(1L);
        customer.setName("客户A");

        OrderItem item = new OrderItem();
        item.setOrderId(1001L);
        item.setProductId(1L);
        item.setQuantity(10);
        item.setUnitPrice(new BigDecimal("100.00"));
        item.setSubtotal(new BigDecimal("1000.00"));

        Product product = new Product();
        product.setProductId(1L);
        product.setName("商品A");

        ApprovalRecord record = new ApprovalRecord();
        record.setOrderId(1001L);
        record.setApproverId(2L);
        record.setResult("APPROVED");

        // Mock行为
        when(orderMapper.selectById(1001L)).thenReturn(order);
        when(customerMapper.selectById(1L)).thenReturn(customer);
        when(orderItemMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(item));
        when(productMapper.selectById(1L)).thenReturn(product);
        when(approvalRecordMapper.selectList(any(QueryWrapper.class))).thenReturn(Collections.singletonList(record));

        // 执行
        OrderDetailVO vo = orderService.getOrderDetail(1001L);

        // 验证
        assertNotNull(vo);
        assertEquals(1001L, vo.getOrderId());
        assertEquals("客户A", vo.getCustomerName());
        assertEquals(1, vo.getItems().size());
        assertEquals(1, vo.getApprovalRecords().size());
    }

    @Test
    void testPageOrderList_Success() {
        // 准备数据
        OrderPageRequestDTO dto = new OrderPageRequestDTO();
        dto.setCustomerId(1L);
        dto.setStatus("APPROVED");
        dto.setPageNum(1);
        dto.setPageSize(10);

        SalesOrder order = new SalesOrder();
        order.setOrderId(1001L);
        order.setCustomerId(1L);
        order.setTotalAmount(new BigDecimal("1000.00"));
        order.setStatus("APPROVED");

        Customer customer = new Customer();
        customer.setCustomerId(1L);
        customer.setName("客户A");

        Page<SalesOrder> orderPage = new Page<>(1, 10);
        orderPage.setRecords(Collections.singletonList(order));
        orderPage.setTotal(100);

        // Mock行为
        when(orderMapper.selectPage(any(Page.class), any(QueryWrapper.class))).thenReturn(orderPage);
        when(customerMapper.selectById(1L)).thenReturn(customer);

        // 执行
        Page<OrderListVO> result = orderService.pageOrderList(dto);

        // 验证
        assertNotNull(result);
        assertEquals(100, result.getTotal());
        assertEquals(1, result.getRecords().size());
        assertEquals("客户A", result.getRecords().get(0).getCustomerName());
        verify(orderMapper, times(1)).selectPage(any(Page.class), any(QueryWrapper.class));
    }
}

6.2 测试说明

  • 覆盖场景:新增testPageOrderList_Success测试分页查询,验证分页参数、查询条件和VO转换。
  • Mock依赖:使用Mockito模拟Mapper层,隔离数据库依赖。
  • 验证逻辑:通过verify检查方法调用次数,确保分页逻辑正确。

七、总结

通过以上步骤,我们完成了一个ERP系统中销售订单管理模块的开发全流程:

  1. 需求分析:明确业务需求,定义功能点和约束。
  2. 数据库设计:设计实体表和关联表,确保数据一致性。
  3. 代码实现:基于MyBatis-Plus实现PO、DTO、VO和Service层,添加分页查询逻辑。
  4. 接口定义:使用ApiFox规范接口,包含入参、出参和权限控制,使用ApiResponse封装返回。
  5. 单元测试:使用Mockito编写全面的单元测试,覆盖核心场景和分页查询。

希望这篇博客能为你的ERP系统开发提供参考!如果有更多需求或优化建议,欢迎留言讨论。