阅读时间约 42 分钟

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

功能开发

Posted by LuckyE on January 13, 2025
开发
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>

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);
}

   <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>
   
@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;
    }
}

@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>

// 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
  })
}

<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>

测试用例设计

正常审批测试

请求体:

{
  "transactionIds": [1001, 1002, 1003],
  "approved": true,
  "approverID": 101
}

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

{
    "code": 200,
    "success": true,
    "message": "审批成功",
    "data": true
}

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

请求体:

{
    "transactionIds": [1004, 1005],
    "approved": false,
    "approverID": 101
}

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

{
    "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>

{
    "transactionIds": [1006],
    "approved": true,
    "approverID": 999 // 不存在的审批人ID
}

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

{
    "code": 400,
    "success": false,
    "message": "审批人不存在",
    "data": null
}

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

请求体:

{
    "transactionIds": [9999], // 不存在的流水ID
    "approved": true,
    "approverID": 101
}

预期响应:

{
    "code": 404,
    "success": false,
    "message": "未找到指定的流水记录",
    "data": null
}

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

请求体:

{
    "transactionIds": [1007],
    "approved": true,
    "approverID": 102 // 无权限的审批人ID
}

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

{
    "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>
   baseUrl: http://your-test-server:port
   token: eyJhbGciOiJIUzI1NiJ9...  // 认证token
  1. <font style="background-color:rgb(250, 250, 250);">添加预请求脚本</font>
   // 如需要在请求前动态获取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>
   // 验证响应状态
   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>



Testimonials

What readers say

先看读者反馈,再直接在当前页面继续讨论。公共留言需要 Waline 服务端;配置后访客只填昵称即可发布。

这类长文如果结构清楚,我会一路读到底。这里最好的地方是把概念、公式和代码示例放在同一篇里。

L
Lin 算法读者

数据库和工程文档的风格很实用,截图、SQL 和说明都能直接拿去复盘项目。

M
Mia 工程笔记党

强化学习相关文章密度很高,但排版如果更清楚,回看体验会更好。这个新版方向是对的。

R
Ryo 深夜学习者

我更喜欢能快速扫到标签、修改时间和文章重点的首页,现在这种卡片视图会比纯列表更容易选读。

C
Chen 知识整理控

代码块只要语言标识和层级做好,技术博客的专业感会立刻上来。

A
Ava 前端同行

评论区不用社交账号强绑定会更愿意留言,尤其是这种偏学习记录的网站。

N
Noah 匿名访客

Quick Identity

Pick a preset and leave a note

留言方式:先选择一个预设身份,再在下方输入评论。当前若显示“需要配置 Waline”,说明站点还缺少可写评论后端。

当前未选择预设身份

Live Discussion Waline 配置完成后,真实评论会加载在下方,移动端和主题切换会同步处理。
WALINE
Loading comments…