ERP系统功能开发全流程:从需求分析到单元测试(含分页逻辑)
本文以ERP系统中销售订单管理模块的开发为例,详细讲解从需求分析、数据库设计、代码实现到单元测试的全流程。我们将模拟产品经理的需求,基于MyBatis-Plus设计实体类、VO、DTO,定义ApiFox接口规范,使用ApiResponse封装返回结果,添加分页查询逻辑,并使用Mockito编写专业的单元测试。
一、业务需求分析
1.1 需求背景
作为一个ERP系统,销售订单管理模块需要支持以下核心功能:
- 订单创建:销售人员创建销售订单,包含客户信息、商品明细、订单总额等。
- 订单审批:订单需经过多级审批(如部门主管、财务)。
- 订单状态管理:支持订单状态流转(草稿、待审批、已审批、已发货、已完成、已取消)。
- 库存校验:创建订单时需校验商品库存是否足够。
- 客户信用检查:订单创建前需检查客户信用额度是否足够。
- 订单分页查询:支持按客户ID或订单状态分页查询订单列表。
1.2 功能需求
-
订单创建:
- 输入:客户ID、商品列表(商品ID、数量)、订单备注。
- 输出:订单ID、订单状态。
- 约束:客户信用额度需满足订单总额;商品库存需足够。
-
订单审批:
- 输入:订单ID、审批人ID、审批结果(通过/拒绝)、审批备注。
- 输出:更新后的订单状态。
-
订单查询:
- 输入:订单ID(详情查询)或客户ID/状态(分页查询)。
- 输出:订单详情(包括商品明细、审批记录)或分页订单列表。
1.3 非功能需求
- 高并发:支持多人同时创建和审批订单。
- 数据一致性:订单状态、库存、信用额度需保持一致。
- 可扩展性:支持后续添加新功能,如折扣、促销等。
二、数据库设计
2.1 实体和关联表设计
根据需求,我们需要以下核心实体和关联表:
实体表
-
客户表 (customer) :
- 存储客户基本信息和信用额度。
- 字段:
customer_id(主键),name,credit_limit,create_time,update_time。
-
商品表 (product) :
- 存储商品信息和库存。
- 字段:
product_id(主键),name,price,stock,create_time,update_time。
-
订单表 (sales_order) :
- 存储订单基本信息。
- 字段:
order_id(主键),customer_id(外键),total_amount,status(草稿/待审批/已审批/已发货/已完成/已取消),remark,create_time,update_time。
-
审批记录表 (approval_record) :
- 存储订单审批历史。
- 字段:
record_id(主键),order_id(外键),approver_id,result(通过/拒绝),remark,create_time。
关联表
-
订单商品明细表 (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_price和subtotal,避免价格波动导致计算不一致。 - 审批记录表:记录每次审批的操作,便于追溯。
- 外键约束:确保数据一致性,如订单必须关联有效客户。
三、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,包含客户名称等前端所需字段。 - 事务管理:
createOrder和approveOrder使用@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系统中销售订单管理模块的开发全流程:
- 需求分析:明确业务需求,定义功能点和约束。
- 数据库设计:设计实体表和关联表,确保数据一致性。
- 代码实现:基于MyBatis-Plus实现PO、DTO、VO和Service层,添加分页查询逻辑。
- 接口定义:使用ApiFox规范接口,包含入参、出参和权限控制,使用
ApiResponse封装返回。 - 单元测试:使用Mockito编写全面的单元测试,覆盖核心场景和分页查询。
希望这篇博客能为你的ERP系统开发提供参考!如果有更多需求或优化建议,欢迎留言讨论。