阅读时间约 45 分钟

仓储管理系统实现审批功能

功能开发

Posted by LuckyE on January 13, 2025
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE InventoryTransaction (
    TransactionID INT PRIMARY KEY IDENTITY(1,1),  -- 流水ID
    ItemID INT NOT NULL,                          -- 物料ID
    WarehouseID INT NOT NULL,                     -- 仓库ID
    TransactionType TINYINT NOT NULL,             -- 交易类型(1:入库, 2:出库, 3:调拨, 4:盘点)
    Quantity DECIMAL(10,2) NOT NULL,              -- 数量
    UnitPrice DECIMAL(10,2),                      -- 单价
    TotalAmount DECIMAL(12,2),                    -- 总金额
    RelatedOrderID VARCHAR(50),                   -- 关联订单号
    OperatorID INT NOT NULL,                      -- 操作人员ID
    DepartmentID INT,                             -- 部门ID
    TransactionTime DATETIME NOT NULL,            -- 交易时间
    RecordTime DATETIME NOT NULL DEFAULT GETDATE(), -- 记录时间
    Notes VARCHAR(500),                           -- 备注
    Status TINYINT NOT NULL DEFAULT 2             -- 状态(0(无效)、1(有效/已审批)、2(待审批)、3(已拒绝))
)
  • <font style="background-color:rgb(250, 250, 250);">状态字段扩展 </font> <font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">:将Status字段含义扩展为:0(无效)、1(有效/已审批)、2(待审批)、3(已拒绝)</font>

<font style="background-color:rgb(250, 250, 250);">MyBatis实现</font>

<font style="background-color:rgb(250, 250, 250);">Mapper接口设计</font>

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface InventoryTransactionMapper {
    // 查询待审批的库存流水记录
    List<InventoryTransaction> findPendingTransactions(Map<String, Object> params);

    // 批量更新流水状态
    int batchUpdateStatus(@Param("ids") List<Integer> transactionIds, 
                          @Param("status") int status, 
                          @Param("approverID") int approverID);

    // 获取流水详情
    InventoryTransaction getTransactionById(int transactionId);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
   <mapper namespace="com.irrigation.warehouse.mapper.InventoryTransactionMapper">
       <select id="findPendingTransactions" resultMap="transactionResultMap">
           SELECT t.*, i.ItemName, w.WarehouseName, o.OperatorName
           FROM InventoryTransaction t
           JOIN Item i ON t.ItemID = i.ItemID
           JOIN Warehouse w ON t.WarehouseID = w.WarehouseID
           JOIN Operator o ON t.OperatorID = o.OperatorID
           WHERE t.Status = 2
           <if test="itemId != null">AND t.ItemID = #{itemId}</if>
           <if test="warehouseId != null">AND t.WarehouseID = #{warehouseId}</if>
           <if test="startDate != null">AND t.TransactionTime >= #{startDate}</if>
           <if test="endDate != null">AND t.TransactionTime <= #{endDate}</if>
           ORDER BY t.TransactionTime DESC
       </select>
     
       <update id="batchUpdateStatus">
           UPDATE InventoryTransaction
           SET Status = #{status},
               Notes = CONCAT(Notes, '; 审批时间: ', CONVERT(VARCHAR, GETDATE(), 120), 
                            ', 审批人: ', (SELECT OperatorName FROM Operator WHERE OperatorID = #{approverID}))
           WHERE TransactionID IN
           <foreach collection="ids" item="id" open="(" separator="," close=")">
               #{id}
           </foreach>
       </update>
   </mapper>
   
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
@Transactional
public class InventoryApprovalServiceImpl implements InventoryApprovalService {

    @Autowired
    private InventoryTransactionMapper transactionMapper;

    @Override
    public Page<InventoryTransaction> getPendingTransactions(TransactionQueryDTO queryParams) {
        // 构建查询参数并执行分页查询
        PageHelper.startPage(queryParams.getPageNum(), queryParams.getPageSize());
        List<InventoryTransaction> transactions = transactionMapper.findPendingTransactions(buildQueryMap(queryParams));
        return new PageInfo<>(transactions).toPage();
    }

    @Override
    public boolean approveTransactions(ApprovalDTO approvalDTO) {
        // 执行批量审批,更新状态
        return transactionMapper.batchUpdateStatus(
            approvalDTO.getTransactionIds(), 
            approvalDTO.isApproved() ? 1 : 3,  // 1=已批准, 3=已拒绝
            approvalDTO.getApproverID()
        ) > 0;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@RestController
@RequestMapping("/api/inventory/approval")
public class InventoryApprovalController {
  
    @Autowired
    private InventoryApprovalService approvalService;
  
    @GetMapping("/pending")
    public ResultDTO<Page<InventoryTransaction>> getPendingTransactions(TransactionQueryDTO queryParams) {
        return ResultDTO.success(approvalService.getPendingTransactions(queryParams));
    }
  
    @PostMapping("/submit")
    public ResultDTO<Boolean> submitApproval(@RequestBody ApprovalDTO approvalDTO) {
        boolean result = approvalService.approveTransactions(approvalDTO);
        return result ? ResultDTO.success(true) : ResultDTO.fail("审批失败,请重试");
    }
}

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">前端Vue实现</font>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// api/inventory.js
import request from '@/utils/request'

export function getPendingTransactions(query) {
  return request({
    url: '/api/inventory/approval/pending',
    method: 'get',
    params: query
  })
}

export function submitApproval(data) {
  return request({
    url: '/api/inventory/approval/submit',
    method: 'post',
    data
  })
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<template>
  <!-- 假设这是已有的库存流水列表页面 -->
  <div class="inventory-list">
  <!-- 已有的搜索功能区域 -->
  <div class="existing-search-area">
  <!-- 现有代码... -->
  </div>

  <!-- 添加的操作按钮区域 -->
  <div class="operation-buttons">
  <el-button 
type="primary" 
icon="el-icon-check" 
:disabled="selectedRows.length === 0"
@click="handleApprove">审批</el-button>
  <el-button 
type="danger" 
icon="el-icon-close" 
:disabled="selectedRows.length === 0"
@click="handleReject">拒绝</el-button>
  </div>

  <!-- 已有的表格组件添加@selection-change事件 -->
  <el-table
ref="inventoryTable"
:data="inventoryList"
@selection-change="handleSelectionChange">
  <!-- 添加选择列 -->
  <el-table-column type="selection" width="55" />

  <!-- 现有的表格列... -->
  </el-table>

  <!-- 现有的分页组件... -->
  </div>
  </template>
  <script>
  // 引入已有的API方法,添加submitApproval方法
  import { submitApproval } from '@/api/inventory'

export default {
  // 扩展现有组件
  data() {
    // 添加选中行数据
    return {
      selectedRows: []
    }
  },
  methods: {
    // 处理表格选择变化
    handleSelectionChange(val) {
      this.selectedRows = val
    },

    // 处理审批操作
    handleApprove() {
      if (this.selectedRows.length === 0) return

      this.$confirm(`确认审批选中的 ${this.selectedRows.length} 条记录?`, '确认信息', {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.submitApprovalRequest(true)
      })
    },

    // 处理拒绝操作
    handleReject() {
      if (this.selectedRows.length === 0) return

      this.$confirm(`确认拒绝选中的 ${this.selectedRows.length} 条记录?`, '确认信息', {
        confirmButtonText: '确认',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.submitApprovalRequest(false)
      })
    },

    // 提交审批请求
    submitApprovalRequest(isApproved) {
      const transactionIds = this.selectedRows.map(item => item.transactionID)

      submitApproval({
        transactionIds,
        approved: isApproved,
        approverID: this.$store.getters.userId
      }).then(() => {
        this.$message.success('操作成功')
        // 刷新列表数据
        this.refreshList()
        // 清空选择
        this.$refs.inventoryTable.clearSelection()
      }).catch(error => {
        this.$message.error('操作失败: ' + error.message)
      })
    },

    // 刷新列表数据(调用现有方法)
    refreshList() {
      // 调用已有的获取列表数据方法
      this.getInventoryList()
    }
  }
}
  </script>
  <style scoped>
  .operation-buttons {
  margin: 10px 0;
  text-align: right;
}
</style>

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">接口测试设计–库存审批接口</font>

<font style="background-color:rgb(250, 250, 250);">接口基本信息</font>

  • <font style="background-color:rgb(250, 250, 250);">接口URL</font><font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">:</font><font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);"> </font><font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">/api/inventory/approval/submit</font>
  • <font style="background-color:rgb(250, 250, 250);">请求方法</font><font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">: POST</font>
  • <font style="background-color:rgb(250, 250, 250);">Content-Type</font><font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">: application/json</font>

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);"></font>

测试用例设计

正常审批测试

请求体:

1
2
3
4
5
6
{
  "transactionIds": [1001, 1002, 1003],
  "approved": true,
  "approverID": 101
}

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">预期响应:</font>

1
2
3
4
5
6
7
{
    "code": 200,
    "success": true,
    "message": "审批成功",
    "data": true
}

<font style="background-color:rgb(250, 250, 250);">正常拒绝测试</font>

请求体:

1
2
3
4
5
6
{
    "transactionIds": [1004, 1005],
    "approved": false,
    "approverID": 101
}

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">预期响应:</font>

1
2
3
4
5
6
7
{
    "code": 200,
    "success": true,
    "message": "拒绝成功",
    "data": true
}

<font style="background-color:rgb(250, 250, 250);">异常测试 - 审批人ID不存在</font>

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">请求体:</font>

1
2
3
4
5
6
{
    "transactionIds": [1006],
    "approved": true,
    "approverID": 999 // 不存在的审批人ID
}

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">预期响应:</font>

1
2
3
4
5
6
7
{
    "code": 400,
    "success": false,
    "message": "审批人不存在",
    "data": null
}

<font style="background-color:rgb(250, 250, 250);">异常测试 - 流水ID不存在</font>

请求体:

1
2
3
4
5
6
{
    "transactionIds": [9999], // 不存在的流水ID
    "approved": true,
    "approverID": 101
}

预期响应:

1
2
3
4
5
6
7
{
    "code": 404,
    "success": false,
    "message": "未找到指定的流水记录",
    "data": null
}

<font style="background-color:rgb(250, 250, 250);">异常测试 - 权限不足</font>

请求体:

1
2
3
4
5
6
{
    "transactionIds": [1007],
    "approved": true,
    "approverID": 102 // 无权限的审批人ID
}

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">预期响应:</font>

1
2
3
4
5
6
7
{
    "code": 403,
    "success": false,
    "message": "无权限执行此操作",
    "data": null
}

<font style="background-color:rgb(250, 250, 250);">测试前置条件</font>

  1. <font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">确保测试数据库中存在ID为1001-1007的待审批记录</font>
  2. <font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">确保ID为101的用户有审批权限,ID为102的用户无审批权限</font>
  3. <font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">确保测试环境的接口认证已正确设置(如需要Token)</font>

<font style="background-color:rgb(250, 250, 250);">Postman测试</font>

  1. <font style="background-color:rgb(250, 250, 250);">设置环境变量</font>
1
2
   baseUrl: http://your-test-server:port
   token: eyJhbGciOiJIUzI1NiJ9...  // 认证token
  1. <font style="background-color:rgb(250, 250, 250);">添加预请求脚本</font>
1
2
3
4
5
6
7
8
9
10
11
12
   // 如需要在请求前动态获取token
   pm.sendRequest({
       url: pm.environment.get("baseUrl") + "/api/auth/login",
       method: "POST",
       header: { "Content-Type": "application/json" },
       body: { mode: "raw", raw: JSON.stringify({ username: "testuser", password: "password" }) }
   }, function(err, res) {
       if (!err) {
           pm.environment.set("token", res.json().data.token);
       }
   });
   
  1. <font style="background-color:rgb(250, 250, 250);">添加测试脚本</font>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   // 验证响应状态
   pm.test("Status code is 200", function() {
       pm.response.to.have.status(200);
   });
   
   // 验证响应内容
   pm.test("Response is successful", function() {
       var jsonData = pm.response.json();
       pm.expect(jsonData.success).to.be.true;
       pm.expect(jsonData.code).to.equal(200);
   });
   
   // 响应时间测试
   pm.test("Response time is acceptable", function() {
       pm.expect(pm.response.responseTime).to.be.below(300);
   });
   

<font style="color:rgb(55, 65, 81);background-color:rgb(250, 250, 250);">

</font>