pan
2025-11-29 fcdbb4d6cc8eb3629b871a4945ff2da599d64107
提交
已添加1个文件
已修改16个文件
2267 ■■■■ 文件已修改
项目代码/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/outbound/BatchPickingConfirm.vue 958 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/CommonEnum/PalletTypeEnum.cs 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/OutboundOrderEnum.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/OutLockStockStatusEnum.cs 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/StockStatusEmun.cs 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/BatchOutBoundDto.cs 169 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/OutboundOrderGetDTO.cs 45 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_DTO/Task/WMSTaskDTO.cs 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundBatchPickingService.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_PickingRecord.cs 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundBatchPickingService.cs 777 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Outbound.cs 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundBatchPickingController.cs 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/outbound/extend/outOrderDetail.vue
@@ -642,18 +642,18 @@
            } catch (err) {
              return;
            }
console.log(this.selection);
            // 4. æž„造请求参数(新增小数字段)
            const keys = this.selection.map((item) => item.id);
            const requestParams = {
              taskIds: keys,
              orderDetailId: keys[0], // åˆ†æ‰¹å‡ºåº“仅支持单条明细
              outboundPlatform: formData.selectedPlatform, // å‡ºåº“站台
              outboundDecimal: formData.outboundDecimal // æ–°å¢žï¼šå°æ•°å­—段传给后端
              batchQuantity: formData.outboundDecimal // æ–°å¢žï¼šå°æ•°å­—段传给后端
            };
            // 5. è°ƒç”¨å‡ºåº“接口
            this.http
              .post("api/Task/ ", requestParams, "数据处理中")
              .post("api/Task/GenerateOutboundBatchTasks", requestParams, "数据处理中")
              .then((x) => {
                if (!x.status) return ElMessage.error(x.message);
                
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/outbound/BatchPickingConfirm.vue
@@ -4,29 +4,8 @@
      <el-page-header @back="goBack">
        <template #content>
          <span class="title">出库拣选确认 - {{ this.$route.query.orderNo }}</span>
          <el-tag v-if="currentBatchNo" type="success" style="margin-left: 10px;">
            å½“前批次: {{ currentBatchNo }}
          </el-tag>
        </template>
      </el-page-header>
    </div>
    <!-- æ‰¹æ¬¡æ“ä½œåŒºåŸŸ -->
    <div class="batch-operations">
      <el-card>
        <div class="batch-actions">
          <el-button type="primary" @click="openBatchAllocateDialog">分批分配</el-button>
          <el-select v-model="selectedBatchNo" placeholder="选择批次" @change="onBatchChange" style="width: 200px; margin-left: 10px;">
            <el-option
              v-for="batch in batchList"
              :key="batch.batchNo"
              :label="`${batch.batchNo} (${batch.batchStatusText})`"
              :value="batch.batchNo">
            </el-option>
          </el-select>
          <el-button type="info" @click="refreshBatchList">刷新批次</el-button>
        </div>
      </el-card>
    </div>
    <!-- æ‰«ç åŒºåŸŸ -->
@@ -52,21 +31,6 @@
          <el-button type="info" @click="openRevertSplitDialog">撤销拆包</el-button>
          <el-button type="info" @click="handleEmptyPallet">取空箱</el-button>
          <el-button type="primary" @click="openBatchReturnDialog">回库</el-button>
        </div>
      </el-card>
    </div>
    <!-- æ‰¹æ¬¡æ±‡æ€»ä¿¡æ¯ -->
    <div class="batch-summary-area" v-if="currentBatchNo">
      <el-card>
        <div class="batch-summary-info">
          <el-tag type="info">批次号: {{ batchSummary.batchNo }}</el-tag>
          <el-tag :type="getBatchStatusType(batchSummary.batchStatus)">
            {{ batchSummary.batchStatusText }}
          </el-tag>
          <el-tag type="warning">分配数量: {{ batchSummary.batchQuantity }}</el-tag>
          <el-tag type="success">完成数量: {{ batchSummary.completedQuantity }}</el-tag>
          <el-tag type="danger">剩余数量: {{ batchSummary.remainingQuantity }}</el-tag>
        </div>
      </el-card>
    </div>
@@ -127,52 +91,6 @@
      </el-row>
    </div>
    <!-- åˆ†æ‰¹åˆ†é…å¼¹çª— -->
    <div v-if="showBatchAllocateDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
        <div class="custom-dialog">
          <div class="custom-dialog-header">
            <h3>分批分配库存</h3>
            <el-button type="text" @click="closeBatchAllocateDialog" class="close-button">×</el-button>
          </div>
          <div class="custom-dialog-body">
            <el-form :model="batchAllocateForm" :rules="batchAllocateFormRules" ref="batchAllocateFormRef" label-width="100px">
              <el-form-item label="订单编号">
                <el-input v-model="batchAllocateForm.orderNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="物料明细" prop="orderDetailId">
                <el-select v-model="batchAllocateForm.orderDetailId" placeholder="选择物料明细" style="width: 100%">
                  <el-option
                    v-for="detail in allocatableDetails"
                    :key="detail.id"
                    :label="`${detail.materielCode} - éœ€æ±‚:${detail.needOutQuantity} å·²åˆ†é…:${detail.allocatedQuantity} å¯åˆ†é…:${detail.availableQuantity}`"
                    :value="detail.id">
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item label="分配数量" prop="batchQuantity">
                <el-input-number
                  v-model="batchAllocateForm.batchQuantity"
                  :min="0.01"
                  :precision="2"
                  :step="1"
                  style="width: 100%"
                  placeholder="输入分配数量">
                </el-input-number>
              </el-form-item>
              <el-form-item label="可分配数量">
                <el-input :value="getAvailableQuantity()" disabled></el-input>
              </el-form-item>
            </el-form>
          </div>
          <div class="custom-dialog-footer">
            <el-button @click="closeBatchAllocateDialog">取消</el-button>
            <el-button type="primary" @click="handleBatchAllocate" :loading="batchAllocateLoading">确认分配</el-button>
          </div>
        </div>
      </div>
    </div>
    <!-- æ‹†åŒ…弹窗 -->
    <div v-if="showCustomSplitDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
@@ -186,20 +104,28 @@
              <el-form-item label="订单编号">
                <el-input v-model="splitForm.orderNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="批次编号">
                <el-input v-model="splitForm.batchNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="托盘编号">
                <el-input v-model="splitForm.palletCode" disabled></el-input>
              </el-form-item>
              <el-form-item label="原条码" prop="originalBarcode">
                <el-input
                  v-model="splitForm.originalBarcode"
                  placeholder="扫描原条码"
                  @keyup.enter.native="onSplitBarcodeScan"
                  @change="onSplitBarcodeScan"
                  clearable>
                </el-input>
                <div style="display: flex; align-items: center; gap: 10px;">
                  <el-input
                    v-model="splitForm.originalBarcode"
                    placeholder="扫描原条码"
                    @keyup.enter.native="onSplitBarcodeScan"
                    @change="onSplitBarcodeScan"
                    clearable
                    style="flex: 1;">
                  </el-input>
                  <!-- æ–°å¢žï¼šæŸ¥çœ‹æ‹†åŒ…链按钮 -->
                  <el-button
  type="primary"
  @click="viewSplitChainFromSplit(splitForm.originalBarcode)"
  :disabled="!splitForm.originalBarcode"
  :loading="splitChainLoading">
  æŸ¥çœ‹æ‹†åŒ…链
</el-button>
                </div>
              </el-form-item>
              <el-form-item label="物料编码">
                <el-input v-model="splitForm.materielCode" disabled></el-input>
@@ -238,15 +164,46 @@
          <div class="custom-dialog-body">
            <el-form :model="revertSplitForm" :rules="revertSplitFormRules" ref="revertSplitFormRef" label-width="100px">
              <el-form-item label="新条码" prop="newBarcode">
                <el-input
                  v-model="revertSplitForm.newBarcode"
                  placeholder="扫描新条码"
                  @keyup.enter.native="onRevertSplitBarcodeScan"
                  @change="onRevertSplitBarcodeScan"
                  clearable>
                </el-input>
                <div style="display: flex; align-items: center; gap: 10px;">
                  <el-input
                    v-model="revertSplitForm.newBarcode"
                    placeholder="扫描新条码"
                    @keyup.enter.native="onRevertSplitBarcodeScan"
                    @change="onRevertSplitBarcodeScan"
                    clearable
                    style="flex: 1;">
                  </el-input>
                  <!-- æ–°å¢žï¼šæŸ¥çœ‹æ‹†åŒ…链按钮 -->
                  <el-button
                    type="primary"
                    @click="viewSplitChain(revertSplitForm.newBarcode)"
                    :disabled="!revertSplitForm.newBarcode">
                    æŸ¥çœ‹æ‹†åŒ…链
                  </el-button>
                </div>
              </el-form-item>
            </el-form>
            <!-- æ–°å¢žï¼šæ‹†åŒ…链简要信息显示 -->
            <div v-if="splitChainInfo.splitChain && splitChainInfo.splitChain.length > 0"
                 style="margin-top: 15px; padding: 10px; background: #f0f9ff; border-radius: 4px;">
              <div style="font-size: 14px; color: #606266;">
                <div>拆包链信息: å…± {{ splitChainInfo.totalSplitTimes }} æ¬¡æ‹†åŒ…</div>
                <div style="margin-top: 5px;">
                  <el-tag
                    v-for="item in splitChainInfo.splitChain.slice(0, 3)"
                    :key="item.newBarcode"
                    :type="item.isReverted ? 'success' : 'primary'"
                    size="small"
                    style="margin-right: 5px;">
                    {{ item.newBarcode }} ({{ item.splitQuantity }})
                  </el-tag>
                  <span v-if="splitChainInfo.splitChain.length > 3" style="color: #909399;">
                    ç­‰ {{ splitChainInfo.splitChain.length }} ä¸ªæ¡ç 
                  </span>
                </div>
              </div>
            </div>
          </div>
          <div class="custom-dialog-footer">
            <el-button @click="closeRevertSplitDialog">取消</el-button>
@@ -256,12 +213,136 @@
      </div>
    </div>
    <!-- æ‹†åŒ…链信息弹窗 -->
<div v-if="showSplitChainDialog" class="custom-dialog-overlay">
  <div class="custom-dialog-wrapper">
    <div class="custom-dialog" style="width: 750px;">
      <div class="custom-dialog-header">
        <h3>拆包链信息</h3>
        <el-button type="text" @click="closeSplitChainDialog" class="close-button">×</el-button>
      </div>
      <div class="custom-dialog-body">
        <!-- æ–°å¢žï¼šæ‹†åŒ…链说明 -->
        <div style="margin-bottom: 15px; padding: 10px; background: #f0f9ff; border-radius: 4px;">
          <div style="display: flex; justify-content: space-between; align-items: center;">
            <div>
              <div style="font-weight: bold; color: #303133;">拆包链说明</div>
              <div style="font-size: 12px; color: #606266; margin-top: 5px;">
                å½“前显示的是从 <el-tag type="primary" size="small">{{ splitChainInfo.originalBarcode }}</el-tag> å¼€å§‹çš„æ‹†åŒ…链
                <br>共 {{ splitChainInfo.totalSplitTimes }} æ¬¡æ‹†åŒ…操作,涉及 {{ splitChainInfo.splitChain.length }} ä¸ªæ¡ç 
              </div>
            </div>
            <el-button
              type="primary"
              size="small"
              @click="findRootChain(splitChainInfo.originalBarcode)"
              v-if="splitChainInfo.chainType !== 'root'">
              æŸ¥æ‰¾å®Œæ•´æ‹†åŒ…链
            </el-button>
          </div>
        </div>
        <div style="margin-bottom: 15px;">
          <el-tag type="info">总拆包次数: {{ splitChainInfo.totalSplitTimes }}</el-tag>
          <el-tag type="warning" style="margin-left: 10px;">
            åŽŸå§‹æ¡ç : {{ splitChainInfo.originalBarcode }}
          </el-tag>
          <el-tag :type="splitChainInfo.chainType === 'root' ? 'success' : 'warning'" style="margin-left: 10px;">
            {{ splitChainInfo.chainType === 'root' ? '完整链' : '分支链' }}
          </el-tag>
        </div>
        <el-table :data="splitChainInfo.splitChain" border height="300">
          <el-table-column type="index" label="序号" width="60" align="center"></el-table-column>
          <el-table-column prop="splitTime" label="拆包时间" width="160">
            <template #default="scope">
              {{ formatDateTime(scope.row.splitTime) }}
            </template>
          </el-table-column>
          <el-table-column prop="originalBarcode" label="原条码" width="140">
            <template #default="scope">
              <el-tag
                :type="scope.row.originalBarcode === splitChainInfo.rootBarcode ? 'success' : 'primary'"
                size="small">
                {{ scope.row.originalBarcode }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="newBarcode" label="新条码" width="140">
            <template #default="scope">
              <el-tag
                :type="scope.row.isReverted ? 'info' : 'warning'"
                size="small">
                {{ scope.row.newBarcode }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column prop="splitQuantity" label="拆包数量" width="100" align="right">
            <template #default="scope">
              {{ scope.row.splitQuantity.toFixed(2) }}
            </template>
          </el-table-column>
          <el-table-column prop="operator" label="操作员" width="100"></el-table-column>
          <el-table-column prop="isReverted" label="状态" width="80" align="center">
            <template #default="scope">
              <el-tag
                :type="scope.row.isReverted ? 'success' : 'danger'"
                size="small">
                {{ scope.row.isReverted ? '已撤销' : '有效' }}
              </el-tag>
            </template>
          </el-table-column>
          <el-table-column label="操作" width="120" align="center">
            <template #default="scope">
              <el-button
                v-if="!scope.row.isReverted"
                type="danger"
                size="mini"
                @click="cancelSingleSplit(scope.row.newBarcode)"
                :disabled="hasPicked(scope.row.newBarcode)">
                å–消
              </el-button>
              <span v-else style="color: #909399;">已撤销</span>
            </template>
          </el-table-column>
        </el-table>
        <!-- æ‰¹é‡æ“ä½œåŒºåŸŸ -->
        <div style="margin-top: 15px; padding: 10px; background: #f5f7fa; border-radius: 4px;">
          <div style="display: flex; justify-content: space-between; align-items: center;">
            <div>
              <span style="font-size: 14px; color: #606266;">
                æ‰¹é‡æ“ä½œ: å¯ä»¥å–消整个拆包链或选择单个拆包记录取消
              </span>
              <div style="font-size: 12px; color: #909399; margin-top: 5px;">
                å®Œæ•´æ‹†åŒ…链包含从最原始条码开始的所有拆包操作
              </div>
            </div>
            <div>
              <el-button
                type="danger"
                @click="cancelWholeSplitChain"
                :disabled="!canCancelWholeChain"
                :loading="revertSplitLoading">
                å–消整个拆包链
              </el-button>
            </div>
          </div>
        </div>
      </div>
      <div class="custom-dialog-footer">
        <el-button @click="closeSplitChainDialog">关闭</el-button>
      </div>
    </div>
  </div>
</div>
    <!-- æ‰¹é‡å›žåº“弹窗 -->
    <div v-if="showBatchReturnDialog" class="custom-dialog-overlay">
      <div class="custom-dialog-wrapper">
        <div class="custom-dialog">
          <div class="custom-dialog-header">
            <h3>批次回库</h3>
            <h3>托盘回库</h3>
            <el-button type="text" @click="closeBatchReturnDialog" class="close-button">×</el-button>
          </div>
          <div class="custom-dialog-body">
@@ -269,8 +350,8 @@
              <el-form-item label="订单编号">
                <el-input v-model="batchReturnForm.orderNo" disabled></el-input>
              </el-form-item>
              <el-form-item label="批次编号">
                <el-input v-model="batchReturnForm.batchNo" disabled></el-input>
              <el-form-item label="托盘编号">
                <el-input v-model="batchReturnForm.palletCode" disabled></el-input>
              </el-form-item>
              <el-form-item label="未拣选数量">
                <el-input v-model="batchReturnForm.unpickedQuantity" disabled></el-input>
@@ -335,36 +416,12 @@
  name: 'BatchOutboundPicking',
  components: {printView},
  data() {
    // éªŒè¯è§„则定义...
    const validateBatchQuantity = (rule, value, callback) => {
      if (value === null || value === undefined || value === '') {
        callback(new Error('请输入分配数量'));
      } else if (value <= 0) {
        callback(new Error('分配数量必须大于0'));
      } else {
        callback();
      }
    };
    const validateOrderDetailId = (rule, value, callback) => {
      if (!value) {
        callback(new Error('请选择物料明细'));
      } else {
        callback();
      }
    };
    return {
      scanData: {
        orderNo: '',
        palletCode: '',
        barcode: '',
        batchNo: ''
        barcode: ''
      },
      currentBatchNo: '', // å½“前批次号
      batchList: [], // æ‰¹æ¬¡åˆ—表
      selectedBatchNo: '', // é€‰ä¸­çš„æ‰¹æ¬¡å·
      batchSummary: {}, // æ‰¹æ¬¡æ±‡æ€»ä¿¡æ¯
      unpickedList: [],
      pickedList: [],
      selectedPickedRows: [],
@@ -376,30 +433,22 @@
      palletStatus: '未知',
      
      // å¼¹çª—状态
      showBatchAllocateDialog: false,
      showCustomSplitDialog: false,
      showRevertSplitDialog: false,
      showBatchReturnDialog: false,
      showEmptyPalletDialog: false,
      showSplitChainDialog: false, // æ–°å¢žï¼šæ‹†åŒ…链信息弹窗
      
      // åŠ è½½çŠ¶æ€
      batchAllocateLoading: false,
      splitLoading: false,
      revertSplitLoading: false,
      batchReturnLoading: false,
      emptypalletOutLoading: false,
      splitChainLoading: false, // æ–°å¢žï¼šæ‹†åŒ…链加载状态
      
      // è¡¨å•数据
      batchAllocateForm: {
        orderNo: '',
        orderDetailId: '',
        batchQuantity: 0
      },
      allocatableDetails: [], // å¯åˆ†é…çš„订单明细
      splitForm: {
        orderNo: '',
        batchNo: '',
        palletCode: '',
        originalBarcode: '',
        materielCode: '',
@@ -413,7 +462,7 @@
      
      batchReturnForm: {
        orderNo: '',
        batchNo: '',
        palletCode: '',
        unpickedCount: 0,
        unpickedQuantity: 0
      },
@@ -423,25 +472,52 @@
        palletCode: ''
      },
      
      // æ–°å¢žï¼šæ‹†åŒ…链相关数据
      splitChainInfo: {
        originalBarcode: '',
        totalSplitTimes: 0,
        splitChain: []
      },
      // éªŒè¯è§„则
      batchAllocateFormRules: {
        orderDetailId: [
          { required: true, validator: validateOrderDetailId, trigger: 'change' }
      splitFormRules: {
        originalBarcode: [
          { required: true, message: '请输入原条码', trigger: 'blur' }
        ],
        batchQuantity: [
          { required: true, validator: validateBatchQuantity, trigger: 'blur' }
        splitQuantity: [
          { required: true, message: '请输入拆包数量', trigger: 'blur' },
          { type: 'number', min: 0.01, message: '拆包数量必须大于0', trigger: 'blur' }
        ]
      },
      
      // å…¶ä»–验证规则...
      revertSplitFormRules: {
        newBarcode: [
          { required: true, message: '请输入新条码', trigger: 'blur' }
        ]
      },
      emptypalletOutFormRules: {
        palletCode: [
          { required: true, message: '请输入托盘码', trigger: 'blur' }
        ]
      },
      isProcessing: false
    }
  },
  computed: {
    // æ˜¯å¦å¯ä»¥å–消整个拆包链
    canCancelWholeChain() {
      return this.splitChainInfo.splitChain &&
             this.splitChainInfo.splitChain.some(item => !item.isReverted);
    }
  },
  mounted() {
    if (this.$route.query.orderNo) {
      this.scanData.orderNo = this.$route.query.orderNo;
      this.batchAllocateForm.orderNo = this.$route.query.orderNo;
      this.loadBatchList();
      this.splitForm.orderNo = this.$route.query.orderNo;
      this.batchReturnForm.orderNo = this.$route.query.orderNo;
      this.emptypalletOutForm.orderNo = this.$route.query.orderNo;
    }
    this.$nextTick(() => {
      this.$refs.palletInput.focus();
@@ -450,150 +526,6 @@
  methods: {
    goBack(){
      this.$router.back()
    },
    // æ‰¹æ¬¡ç›¸å…³æ–¹æ³•
    async loadBatchList() {
      try {
        const res = await http.post('/api/BatchOutbound/order-batch-list', {
          orderNo: this.scanData.orderNo
        });
        if (res.status) {
          this.batchList = res.data || [];
          if (this.batchList.length > 0) {
            this.selectedBatchNo = this.batchList[0].batchNo;
            this.currentBatchNo = this.selectedBatchNo;
            this.scanData.batchNo = this.selectedBatchNo;
            this.loadBatchData();
          }
        }
      } catch (error) {
        this.$message.error('加载批次列表失败');
      }
    },
    async refreshBatchList() {
      await this.loadBatchList();
      this.$message.success('批次列表已刷新');
    },
    onBatchChange(batchNo) {
      this.currentBatchNo = batchNo;
      this.scanData.batchNo = batchNo;
      this.loadBatchData();
    },
    async loadBatchData() {
      if (!this.currentBatchNo) return;
      await this.loadBatchSummary();
      await this.loadUnpickedList();
      await this.loadPickedList();
    },
    async loadBatchSummary() {
      try {
        const res = await http.post('/api/BatchOutbound/batch-summary', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
        });
        if (res.status) {
          this.batchSummary = res.data || {};
        }
      } catch (error) {
        this.$message.error('加载批次汇总失败');
      }
    },
    async loadUnpickedList() {
      try {
        const res = await http.post('/api/BatchOutbound/batch-unpicked-list', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
        });
        this.unpickedList = res.data || [];
        this.summary.unpickedCount = this.unpickedList.length;
        this.summary.unpickedQuantity = this.unpickedList.reduce((sum, item) => sum + (item.remainQuantity || 0), 0);
      } catch (error) {
        this.$message.error('加载未拣选列表失败');
      }
    },
    async loadPickedList() {
      try {
        const res = await http.post('/api/BatchOutbound/batch-picked-list', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
        });
        this.pickedList = res.data || [];
        this.summary.pickedCount = this.pickedList.length;
      } catch (error) {
        this.$message.error('加载已拣选列表失败');
      }
    },
    getBatchStatusType(status) {
      const statusMap = {
        0: 'info', // åˆ†é…ä¸­
        1: 'warning', // æ‰§è¡Œä¸­
        2: 'success', // å·²å®Œæˆ
        3: 'danger' // å·²å›žåº“
      };
      return statusMap[status] || 'info';
    },
    // åˆ†æ‰¹åˆ†é…ç›¸å…³æ–¹æ³•
    async openBatchAllocateDialog() {
      this.showBatchAllocateDialog = true;
      await this.loadAllocatableDetails();
      this.batchAllocateForm.orderDetailId = '';
      this.batchAllocateForm.batchQuantity = 0;
    },
    async loadAllocatableDetails() {
      try {
        const res = await http.post('/api/BatchOutbound/allocatable-order-details', {
          orderNo: this.scanData.orderNo
        });
        if (res.status) {
          this.allocatableDetails = res.data || [];
        }
      } catch (error) {
        this.$message.error('加载可分配明细失败');
      }
    },
    getAvailableQuantity() {
      const detail = this.allocatableDetails.find(d => d.id === this.batchAllocateForm.orderDetailId);
      return detail ? detail.availableQuantity : 0;
    },
    async handleBatchAllocate() {
      if (this.$refs.batchAllocateFormRef) {
        this.$refs.batchAllocateFormRef.validate(async (valid) => {
          if (valid) {
            this.batchAllocateLoading = true;
            try {
              const res = await http.post('/api/BatchOutbound/batch-allocate-stock', this.batchAllocateForm);
              if (res.status) {
                this.$message.success('分批分配成功');
                this.showBatchAllocateDialog = false;
                await this.loadBatchList(); // åˆ·æ–°æ‰¹æ¬¡åˆ—表
              } else {
                this.$message.error(res.message || '分批分配失败');
              }
            } catch (error) {
              this.$message.error('分批分配失败');
            } finally {
              this.batchAllocateLoading = false;
            }
          }
        });
      }
    },
    closeBatchAllocateDialog() {
      this.showBatchAllocateDialog = false;
    },
    // åˆ†æ‹£ç›¸å…³æ–¹æ³•
@@ -606,19 +538,18 @@
        return;
      }
      if (!this.currentBatchNo) {
        this.$message.warning('请先选择批次');
        return;
      }
      this.isProcessing = true;
      
      try {
        const res = await http.post('/api/BatchOutbound/confirm-picking', this.scanData);
        const res = await http.post('/api/OutboundBatchPicking/confirm-picking', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode,
          barcode: this.scanData.barcode
        });
        if (res.status) {
          this.$message.success('拣选确认成功');
          this.scanData.barcode = '';
          await this.loadBatchData();
          await this.loadPalletData();
          if(res.data && res.data.splitResults && res.data.splitResults.length>0){
            this.$refs.childs.open(res.data.splitResults);
          }
@@ -643,14 +574,9 @@
        this.$message.warning('请先扫描托盘码');
        return;
      }
      if (!this.currentBatchNo) {
        this.$message.warning('请先选择批次');
        return;
      }
      this.showCustomSplitDialog = true;
      this.resetSplitForm();
      this.splitForm.orderNo = this.scanData.orderNo;
      this.splitForm.batchNo = this.currentBatchNo;
      this.splitForm.palletCode = this.scanData.palletCode;
    },
@@ -659,9 +585,9 @@
      this.splitForm.originalBarcode = this.splitForm.originalBarcode.replace(/\n/g, '').trim();
      try {
        const res = await http.post('/api/BatchOutbound/split-package-info', {
        const res = await http.post('/api/OutboundBatchPicking/split-package-info', {
          orderNo: this.splitForm.orderNo,
          batchNo: this.splitForm.batchNo,
          palletCode: this.splitForm.palletCode,
          barcode: this.splitForm.originalBarcode
        });
@@ -683,11 +609,16 @@
          if (valid) {
            this.splitLoading = true;
            try {
              const res = await http.post('/api/BatchOutbound/manual-split-package', this.splitForm);
              const res = await http.post('/api/OutboundBatchPicking/split-package', {
                orderNo: this.splitForm.orderNo,
                palletCode: this.splitForm.palletCode,
                originalBarcode: this.splitForm.originalBarcode,
                splitQuantity: this.splitForm.splitQuantity
              });
              if (res.status) {
                this.$message.success('拆包成功');
                this.showCustomSplitDialog = false;
                await this.loadBatchData();
                await this.loadPalletData();
              } else {
                this.$message.error(res.message || '拆包失败');
              }
@@ -700,11 +631,28 @@
        });
      }
    },
// åœ¨æ‹†åŒ…弹窗中查看拆包链
async viewSplitChainFromSplit(barcode) {
  if (!barcode) {
    this.$message.warning('请先输入条码');
    return;
  }
  // å…ˆå…³é—­æ‹†åŒ…弹窗
  this.closeCustomSplitDialog();
  await this.$nextTick();
  // ç„¶åŽæ‰“开拆包链信息弹窗
  await this.viewSplitChain(barcode);
},
    // æ’¤é”€æ‹†åŒ…
    async onRevertSplitBarcodeScan() {
      if (!this.revertSplitForm.newBarcode) return;
      this.revertSplitForm.newBarcode = this.revertSplitForm.newBarcode.replace(/\n/g, '').trim();
      // æ–°å¢žï¼šæ‰«æåŽè‡ªåŠ¨æ˜¾ç¤ºæ‹†åŒ…é“¾ä¿¡æ¯
      await this.viewSplitChain(this.revertSplitForm.newBarcode);
    },
    async handleRevertSplit() {
@@ -713,15 +661,15 @@
          if (valid) {
            this.revertSplitLoading = true;
            try {
              const res = await http.post('/api/BatchOutbound/cancel-split-package', {
              const res = await http.post('/api/OutboundBatchPicking/cancel-split', {
                orderNo: this.scanData.orderNo,
                batchNo: this.currentBatchNo,
                palletCode: this.scanData.palletCode,
                newBarcode: this.revertSplitForm.newBarcode
              });
              if (res.status) {
                this.$message.success('撤销拆包成功');
                this.showRevertSplitDialog = false;
                await this.loadBatchData();
                await this.loadPalletData();
              } else {
                this.$message.error(res.message || '撤销拆包失败');
              }
@@ -734,16 +682,238 @@
        });
      }
    },
// æŸ¥æ‰¾å®Œæ•´æ‹†åŒ…链(从根条码开始)
async findRootChain(currentBarcode) {
  this.splitChainLoading = true;
  try {
    const res = await http.post('/api/OutboundBatchPicking/find-root-split-chain', {
      orderNo: this.scanData.orderNo,
      barcode: currentBarcode
    });
    if (res.status) {
      this.splitChainInfo = res.data;
      this.$message.success('已加载完整拆包链');
    } else {
      this.$message.error(res.message || '查找完整拆包链失败');
    }
  } catch (error) {
    this.$message.error('查找完整拆包链失败');
  } finally {
    this.splitChainLoading = false;
  }
},
    // æ‹†åŒ…链相关方法
   // æŸ¥çœ‹æ‹†åŒ…链信息
async viewSplitChain(barcode) {
  if (!barcode) {
    this.$message.warning('请先输入条码');
    return;
  }
  this.splitChainLoading = true;
  try {
    const res = await http.post('/api/OutboundBatchPicking/split-package-chain-info', {
      orderNo: this.scanData.orderNo,
      barcode: barcode
    });
    if (res.status) {
      this.splitChainInfo = res.data;
      // æ˜¾ç¤ºæç¤ºä¿¡æ¯ï¼Œå‘Šè¯‰ç”¨æˆ·è¿™æ˜¯ä»€ä¹ˆç±»åž‹çš„æ‹†åŒ…链
      let chainType = "当前条码的拆包链";
      if (this.splitChainInfo.chainType === 'root') {
        chainType = "完整拆包链(从原始条码开始)";
      } else if (this.splitChainInfo.chainType === 'branch') {
        chainType = "分支拆包链";
      }
      this.$message.info(`已加载${chainType},共${this.splitChainInfo.totalSplitTimes}次拆包`);
      this.showSplitChainDialog = true;
    } else {
      this.$message.error(res.message || '获取拆包链信息失败');
    }
  } catch (error) {
    this.$message.error('获取拆包链信息失败');
  } finally {
    this.splitChainLoading = false;
  }
},
    // å…³é—­æ‹†åŒ…链信息弹窗
    closeSplitChainDialog() {
      this.showSplitChainDialog = false;
    },
    // åœ¨æ’¤é”€æ‹†åŒ…弹窗中查看拆包链
async viewSplitChainFromRevert(barcode) {
  if (!barcode) {
    this.$message.warning('请先输入条码');
    return;
  }
  // å…ˆå…³é—­æ’¤é”€æ‹†åŒ…弹窗
  this.closeRevertSplitDialog();
  await this.$nextTick();
  // ç„¶åŽæ‰“开拆包链信息弹窗
  await this.viewSplitChain(barcode);
},
// å¿«é€Ÿé‡æ–°æ‰“开拆包链弹窗
async quickReopenSplitChainDialog(barcode) {
  if (!barcode) return;
  this.showSplitChainDialog = true;
  this.splitChainLoading = true;
  try {
    const res = await http.post('/api/OutboundBatchPicking/split-package-chain-info', {
      orderNo: this.scanData.orderNo,
      barcode: barcode
    });
    if (res.status) {
      this.splitChainInfo = res.data;
    }
  } catch (error) {
    console.error('重新加载拆包链信息失败:', error);
  } finally {
    this.splitChainLoading = false;
  }
},
    // å–消单个拆包记录
async cancelSingleSplit(newBarcode) {
  // å…ˆè®°å½•当前信息,然后关闭弹窗
  const originalBarcode = this.splitChainInfo.originalBarcode;
  this.closeSplitChainDialog();
  await this.$nextTick();
  try {
    await this.$confirm(
      `确定要取消条码 ${newBarcode} çš„æ‹†åŒ…操作吗?`,
      '取消单个拆包',
      {
        confirmButtonText: '确定取消',
        cancelButtonText: '再想想',
        type: 'warning'
      }
    );
    this.revertSplitLoading = true;
    const res = await http.post('/api/OutboundBatchPicking/cancel-split', {
      orderNo: this.scanData.orderNo,
      palletCode: this.scanData.palletCode,
      newBarcode: newBarcode
    });
    if (res.status) {
      this.$message.success('取消拆包成功');
      await this.loadPalletData();
      // é‡æ–°æ‰“开弹窗显示更新后的状态
      await this.viewSplitChain(originalBarcode);
    } else {
      this.$message.error(res.message || '取消拆包失败');
      await this.viewSplitChain(originalBarcode);
    }
  } catch (error) {
    if (error === 'cancel') {
      // ç”¨æˆ·å–消后重新打开弹窗
      await this.viewSplitChain(originalBarcode);
    } else {
      this.$message.error('取消拆包失败');
      await this.viewSplitChain(originalBarcode);
    }
  } finally {
    this.revertSplitLoading = false;
  }
},
// å–消整个拆包链
async cancelWholeSplitChain() {
  // å…ˆè®°å½•当前拆包链信息,然后关闭弹窗
  const originalBarcode = this.splitChainInfo.originalBarcode;
  this.closeSplitChainDialog();
  // ç»™ä¸€ç‚¹æ—¶é—´è®©å¼¹çª—完全关闭
  await this.$nextTick();
  try {
    // çŽ°åœ¨æ˜¾ç¤ºç¡®è®¤å¯¹è¯æ¡†ï¼Œç¡®ä¿å®ƒåœ¨æœ€å‰é¢
    await this.$confirm(
      `确定要取消整个拆包链吗?\n这将取消从条码 ${originalBarcode} å¼€å§‹çš„æ‰€æœ‰æ‹†åŒ…操作。`,
      '取消拆包链确认',
      {
        confirmButtonText: '确定取消',
        cancelButtonText: '再想想',
        type: 'warning',
        center: true,
        closeOnClickModal: false
      }
    );
    // ç”¨æˆ·ç¡®è®¤åŽæ‰§è¡Œå–消操作
    this.revertSplitLoading = true;
    const res = await http.post('/api/OutboundBatchPicking/cancel-split-chain', {
      orderNo: this.scanData.orderNo,
      palletCode: this.scanData.palletCode,
      startBarcode: originalBarcode
    });
    console.log('取消拆包链响应:', res);
    if (res.status) {
      this.$message.success('取消拆包链成功');
      await this.loadPalletData();
      // å¯é€‰ï¼šé‡æ–°æ‰“开拆包链信息弹窗显示更新后的状态
      // await this.viewSplitChain(originalBarcode);
    } else {
      this.$message.error(res.message || '取消拆包链失败');
      // å¤±è´¥åŽé‡æ–°æ‰“开弹窗
      await this.viewSplitChain(originalBarcode);
    }
  } catch (error) {
    // ç”¨æˆ·å–消操作
    if (error === 'cancel') {
      console.log('用户取消了拆包链操作');
      // ç”¨æˆ·å–消后重新打开弹窗
      await this.viewSplitChain(originalBarcode);
    } else {
      console.error('取消拆包链错误:', error);
      this.$message.error('取消拆包链失败: ' + error.message);
      // å‡ºé”™åŽé‡æ–°æ‰“开弹窗
      await this.viewSplitChain(originalBarcode);
    }
  } finally {
    this.revertSplitLoading = false;
  }
},
    // æ£€æŸ¥æ¡ç æ˜¯å¦å·²è¢«åˆ†æ‹£
    hasPicked(barcode) {
      return this.pickedList.some(item => item.currentBarcode === barcode);
    },
    // æ ¼å¼åŒ–日期时间
    formatDateTime(dateTime) {
      if (!dateTime) return '';
      const date = new Date(dateTime);
      return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`;
    },
    // å›žåº“相关方法
    openBatchReturnDialog() {
      if (!this.currentBatchNo) {
        this.$message.warning('请先选择批次');
      if (!this.scanData.palletCode) {
        this.$message.warning('请先扫描托盘码');
        return;
      }
      this.showBatchReturnDialog = true;
      this.batchReturnForm.orderNo = this.scanData.orderNo;
      this.batchReturnForm.batchNo = this.currentBatchNo;
      this.batchReturnForm.palletCode = this.scanData.palletCode;
      this.batchReturnForm.unpickedCount = this.summary.unpickedCount;
      this.batchReturnForm.unpickedQuantity = this.summary.unpickedQuantity;
    },
@@ -751,33 +921,42 @@
    async handleBatchReturnConfirm() {
      this.batchReturnLoading = true;
      try {
        const res = await http.post('/api/BatchOutbound/batch-return-stock', {
        const res = await http.post('/api/OutboundBatchPicking/return-stock', {
          orderNo: this.scanData.orderNo,
          batchNo: this.currentBatchNo
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          this.$message.success('批次回库成功');
          this.$message.success('回库成功');
          this.showBatchReturnDialog = false;
          await this.loadBatchData();
          await this.loadPalletData();
        } else {
          this.$message.error(res.message || '批次回库失败');
          this.$message.error(res.message || '回库失败');
        }
      } catch (error) {
        this.$message.error('批次回库失败');
        this.$message.error('回库失败');
      } finally {
        this.batchReturnLoading = false;
      }
    },
    // å–空箱方法
    handleEmptyPallet() {
      this.showEmptyPalletDialog = true;
      this.emptypalletOutForm.orderNo = this.scanData.orderNo;
      this.emptypalletOutForm.palletCode = '';
    },
    async handleEmptyPalletConfirm() {
      this.emptypalletOutLoading = true;
      try {
        const res = await http.post('/api/BatchOutbound/remove-empty-pallet', this.emptypalletOutForm);
        const res = await http.post('/api/OutboundBatchPicking/remove-empty-pallet', {
          orderNo: this.emptypalletOutForm.orderNo,
          palletCode: this.emptypalletOutForm.palletCode
        });
        if (res.status) {
          this.$message.success('取走空箱成功');
          this.showEmptyPalletDialog = false;
          await this.loadBatchData();
          await this.loadPalletData();
        } else {
          this.$message.error(res.message || '取走空箱失败');
        }
@@ -788,32 +967,69 @@
      }
    },
    // å…¶ä»–原有方法...
    // æ•°æ®åŠ è½½æ–¹æ³•
    async loadPalletData() {
      if (!this.scanData.orderNo || !this.scanData.palletCode) return;
      await this.loadUnpickedList();
      await this.loadPickedList();
      await this.loadPalletStatus();
    },
    async loadUnpickedList() {
      try {
        const res = await http.post('/api/OutboundBatchPicking/pallet-locks', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          this.unpickedList = res.data || [];
          this.summary.unpickedCount = this.unpickedList.length;
          this.summary.unpickedQuantity = this.unpickedList.reduce((sum, item) => sum + (item.remainQuantity || 0), 0);
        }
      } catch (error) {
        this.$message.error('加载未拣选列表失败');
      }
    },
    async loadPickedList() {
      try {
        const res = await http.post('/api/OutboundBatchPicking/pallet-picked-list', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          this.pickedList = res.data || [];
          this.summary.pickedCount = this.pickedList.length;
        }
      } catch (error) {
        this.$message.error('加载已拣选列表失败');
      }
    },
    async loadPalletStatus() {
      try {
        const res = await http.post('/api/OutboundBatchPicking/pallet-status', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status) {
          this.palletStatus = res.data.statusText || '未知';
        }
      } catch (error) {
        this.palletStatus = '未知';
      }
    },
    // æ‰«ç ç›¸å…³æ–¹æ³•
    onPalletScan() {
      this.scanData.palletCode = this.scanData.palletCode.replace(/\n/g, '').trim();
      if (!this.scanData.palletCode) return;
      
      this.loadActiveBatch();
      this.loadPalletData();
      this.$nextTick(() => {
        this.$refs.barcodeInput.focus();
      });
    },
    async loadActiveBatch() {
      try {
        const res = await http.post('/api/BatchOutbound/active-batch', {
          orderNo: this.scanData.orderNo,
          palletCode: this.scanData.palletCode
        });
        if (res.status && res.data) {
          this.currentBatchNo = res.data.batchNo;
          this.scanData.batchNo = res.data.batchNo;
          this.selectedBatchNo = res.data.batchNo;
          await this.loadBatchData();
        }
      } catch (error) {
        console.log('获取活跃批次失败,可能托盘没有关联批次');
      }
    },
    onBarcodeScan() {
@@ -853,9 +1069,8 @@
        try {
          for (const row of this.selectedPickedRows) {
            try {
              const res = await http.post('/api/BatchOutbound/cancel-picking', {
              const res = await http.post('/api/OutboundBatchPicking/cancel-picking', {
                orderNo: this.scanData.orderNo,
                batchNo: this.currentBatchNo,
                palletCode: this.scanData.palletCode,
                barcode: row.currentBarcode
              });
@@ -867,7 +1082,7 @@
            }
          }        
          this.$message.success('批量取消完成');
          await this.loadBatchData();
          await this.loadPalletData();
          this.selectedPickedRows = [];
        } catch (error) {
          this.$message.error('批量取消操作失败');
@@ -902,10 +1117,9 @@
      this.showBatchReturnDialog = false;
    },
    openEmptyPalletDialog() {
      this.showEmptyPalletDialog = true;
      this.emptypalletOutForm.orderNo = this.scanData.orderNo;
      this.emptypalletOutForm.palletCode = '';
    onEmptyPalletScan() {
      if (!this.emptypalletOutForm.palletCode) return;
      this.emptypalletOutForm.palletCode = this.emptypalletOutForm.palletCode.replace(/\n/g, '').trim();
    },
    closeEmptyPalletDialog() {
@@ -913,9 +1127,8 @@
      this.emptypalletOutForm.palletCode = '';
    },
    onEmptyPalletScan() {
      if (!this.emptypalletOutForm.palletCode) return;
      this.emptypalletOutForm.palletCode = this.emptypalletOutForm.palletCode.replace(/\n/g, '').trim();
    parentcall() {
      // æ‰“印回调
    }
  }
})
@@ -924,26 +1137,6 @@
<style scoped>
.OutboundPicking-container {
  padding: 20px;
}
.batch-operations {
  margin-bottom: 15px;
}
.batch-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}
.batch-summary-area {
  margin-bottom: 15px;
}
.batch-summary-info {
  display: flex;
  gap: 15px;
  flex-wrap: wrap;
}
.scanner-form {
@@ -977,6 +1170,18 @@
}
/* è‡ªå®šä¹‰å¼¹çª—样式 */
:deep(.el-message-box) {
  z-index: 10010 !important;
}
:deep(.el-overlay) {
  z-index: 10009 !important;
}
:deep(.el-message) {
  z-index: 10011 !important;
}
.custom-dialog-overlay {
  position: fixed;
  top: 0;
@@ -987,12 +1192,12 @@
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
  z-index: 2000; /* ä¿æŒä¸€ä¸ªåˆç†çš„ z-index */
}
.custom-dialog-wrapper {
  position: relative;
  z-index: 10000;
  z-index: 2001;
}
.custom-dialog {
@@ -1058,7 +1263,14 @@
    flex-direction: column;
    align-items: stretch;
  }
  /* ç¡®ä¿ç¡®è®¤å¯¹è¯æ¡†åœ¨æœ€å‰é¢ */
.el-message-box__wrapper {
  z-index: 10001 !important;
}
.el-message {
  z-index: 10002 !important;
}
  .scanner-form .el-input {
    width: 100%;
  }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/CommonEnum/PalletTypeEnum.cs
@@ -35,4 +35,18 @@
        /// </summary>
        LargestPallet = 4
    }
    public enum PalletStatusEnum
    {
        æœªå¼€å§‹ = 0,
        æ‹£é€‰ä¸­ = 1,
        å·²å®Œæˆ = 2,
        æ— ä»»åŠ¡ = 3
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/OrderEnum/OutboundOrderEnum.cs
@@ -103,4 +103,6 @@
        [Description("其他出库单")]
        Other = 235
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/OutLockStockStatusEnum.cs
@@ -35,6 +35,8 @@
        å·²æ‹£é€‰ = 3,
        å·²å›žåº“ = 4,
    }
    public enum OutLockStockStatusEnum
    {
        [Description("已分配")]
@@ -67,6 +69,9 @@
        [Description("已释放")]
        å·²é‡Šæ”¾ =9,
        [Description("已取走")]
        å·²å–èµ° =10,
        [Description("撤销")]
        æ’¤é”€ = 99
    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Common/StockEnum/StockStatusEmun.cs
@@ -40,6 +40,9 @@
        Lock,
    }
    /// <summary>
    /// åº“存状态: <br/>
    /// 1,组盘暂存<br/>
@@ -98,7 +101,10 @@
        [Description("盘点库存完成")]
        ç›˜ç‚¹åº“存完成 = 32,
        [Description("组盘撤销")]
        [Description("已清理")]
        å·²æ¸…理 = 33,
       [Description("组盘撤销")]
        ç»„盘撤销 = 99,
        [Description("入库撤销")]
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/BatchOutBoundDto.cs
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,169 @@
using SqlSugar;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WIDESEA_DTO.Outbound
{
    public class PalletLockInfoDto
    {
        public int Id { get; set; }
        public string OrderNo { get; set; }
        public string BatchNo { get; set; }
        public string MaterielCode { get; set; }
        public string CurrentBarcode { get; set; }
        public decimal AssignQuantity { get; set; }
        public decimal PickedQty { get; set; }
        public int Status { get; set; }
        public string LocationCode { get; set; }
        public string PalletCode { get; set; }
        public bool CanSplit { get; set; }
        public bool CanPick { get; set; }
    }
    #region è¯·æ±‚DTO
    public class ConfirmPickingRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "条码不能为空")]
        public string Barcode { get; set; }
    }
    public class CancelPickingRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "条码不能为空")]
        public string Barcode { get; set; }
        public int PickingHistoryId { get; set; }
    }
    public class CancelSplitRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "新条码不能为空")]
        public string NewBarcode { get; set; }
    }
    public class ReturnStockRequest
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    public class CancelSplitDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "新条码不能为空")]
        public string NewBarcode { get; set; }
    }
    public class ReturnStockDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    public class PalletLocksDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    #endregion
    #region DTOç±»
    public class PalletPickedInfoDto
    {
        public int Id { get; set; }
        public string OrderNo { get; set; }
        public int OrderDetailId { get; set; }
        public string PalletCode { get; set; }
        public string Barcode { get; set; }
        public string MaterielCode { get; set; }
        public decimal PickedQty { get; set; }
        public DateTime PickTime { get; set; }
        public string Operator { get; set; }
        public string LocationCode { get; set; }
    }
    public class PalletStatusDto
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public int Status { get; set; }
        public string StatusText { get; set; }
        public int TotalItems { get; set; }
        public int CompletedItems { get; set; }
        public int PendingItems { get; set; }
    }
    public class SplitPackageInfoDto
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public string Barcode { get; set; }
        public string MaterielCode { get; set; }
        public decimal RemainQuantity { get; set; }
        public decimal AssignQuantity { get; set; }
        public decimal PickedQty { get; set; }
    }
    public class EmptyPalletRemovalDto
    {
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public DateTime RemovalTime { get; set; }
        public string Operator { get; set; }
        public int CompletedItemsCount { get; set; }
        public decimal TotalPickedQuantity { get; set; }
    }
    public class RemoveEmptyPalletDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
    }
    #endregion
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Outbound/OutboundOrderGetDTO.cs
@@ -2,6 +2,7 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -37,8 +38,20 @@
    {
        public int OutStockLockInfoId { get; set; }
        public string MaterielCode { get; set; }
        public decimal SplitQuantity { get; set; }
        public string Operator { get; set; }
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "原条码不能为空")]
        public string OriginalBarcode { get; set; }
        [Range(0.001, double.MaxValue, ErrorMessage = "拆包数量必须大于0")]
        public decimal SplitQuantity { get; set; }
    }
    public class ConfirmPickingDto
    {
@@ -77,10 +90,7 @@
        public string OrderNo { get; set; }
    }
    public class CancelPickingRequest
    {
        public int PickingHistoryId { get; set; }
    }
    public class BackToStockRequest
    {
@@ -261,6 +271,29 @@
        public string Barcode { get; set; }
    }
    public class CancelSplitChainDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "托盘号不能为空")]
        public string PalletCode { get; set; }
        [Required(ErrorMessage = "起始条码不能为空")]
        public string StartBarcode { get; set; }
    }
    public class SplitPackageChainInfoRequestDto
    {
        [Required(ErrorMessage = "订单号不能为空")]
        public string OrderNo { get; set; }
        [Required(ErrorMessage = "条码不能为空")]
        public string Barcode { get; set; }
    }
    public class SplitPackageDto
    {
        public string OrderNo { get; set; }
@@ -295,4 +328,6 @@
        //public decimal SplitQuantity { get; set; }
        //public decimal RemainQuantity { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_DTO/Task/WMSTaskDTO.cs
@@ -81,4 +81,15 @@
        public int[] taskIds { get; set; }
    }
    public class GenerateOutboundBatchTasksDto
    {
        public string orderNo { get; set; }
        public int orderDetailId { get; set; }
        public decimal batchQuantity { get; set; }
        public string outboundPlatform { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_IOutboundService/IOutboundBatchPickingService.cs
@@ -1,5 +1,6 @@
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_DTO.Outbound;
using WIDESEA_Model.Models;
namespace WIDESEA_IOutboundService
@@ -11,7 +12,18 @@
        Task<WebResponseContent> BatchReturnStock(string orderNo, string palletCode);
        Task<WebResponseContent> CancelPicking(string orderNo, string palletCode, string barcode);
        Task<WebResponseContent> CancelSplitPackage(string orderNo, string palletCode, string newBarcode);
        Task<WebResponseContent> CancelSplitPackageChain(string orderNo, string palletCode, string startBarcode);
        Task<List<Dt_SplitPackageRecord>> GetSplitPackageChain(string orderNo, string startBarcode);
        Task<string> FindRootBarcode(string orderNo, string startBarcode);
        Task<WebResponseContent> GetSplitPackageChainInfo(string orderNo, string barcode);
        Task<WebResponseContent> ConfirmBatchPicking(string orderNo, string palletCode, string barcode);
        Task<List<PalletLockInfoDto>> GetPalletLockInfos(string orderNo, string palletCode);
        Task<List<PalletPickedInfoDto>> GetPalletPickedList(string orderNo, string palletCode);
        Task<PalletStatusDto> GetPalletStatus(string orderNo, string palletCode);
        Task<SplitPackageInfoDto> GetSplitPackageInfo(string orderNo, string palletCode, string barcode);
        Task<WebResponseContent> ManualSplitPackage(string orderNo, string palletCode, string originalBarcode, decimal splitQuantity);
        Task<WebResponseContent> RemoveEmptyPallet(string orderNo, string palletCode);
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_ITaskInfoService/ITaskService.cs
@@ -48,6 +48,8 @@
        WebResponseContent GenerateOutboundTask(int orderDetailId, List<StockSelectViewDTO> stockSelectViews);
        Task<WebResponseContent> GenerateOutboundBatchTasksAsync(int orderDetailId, decimal batchQuantity, string outStation);
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_Model/Models/Outbound/Dt_PickingRecord.cs
@@ -122,6 +122,28 @@
       public decimal StockBeforeSplit { get; set; }
        public decimal AssignBeforeSplit { get; set; }
    }
    /// <summary>
    /// ç©ºç®±å–走记录表
    /// </summary>
    public class Dt_EmptyPalletRemoval
    {
        [SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
        public int Id { get; set; }
        public string OrderNo { get; set; }
        public string PalletCode { get; set; }
        public DateTime RemovalTime { get; set; }
        public string Operator { get; set; }
        public int CompletedItemsCount { get; set; }
        public decimal TotalPickedQuantity { get; set; }
    }
}
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_OutboundService/OutboundBatchPickingService.cs
@@ -6,11 +6,13 @@
using System.Text;
using System.Threading.Tasks;
using WIDESEA_BasicService;
using WIDESEA_Common.CommonEnum;
using WIDESEA_Common.OrderEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Core;
using WIDESEA_Core.BaseRepository;
using WIDESEA_Core.BaseServices;
using WIDESEA_DTO.Outbound;
using WIDESEA_IAllocateService;
using WIDESEA_IBasicService;
using WIDESEA_IOutboundService;
@@ -18,6 +20,7 @@
using WIDESEA_Model.Models;
using WIDESEA_Model.Models.Basic;
using WIDESEA_Model.Models.Outbound;
using static WIDESEA_OutboundService.OutboundBatchPickingService;
namespace WIDESEA_OutboundService
{
@@ -61,7 +64,7 @@
        public OutboundBatchPickingService(IRepository<Dt_PickingRecord> BaseDal, IUnitOfWorkManage unitOfWorkManage, IStockInfoService stockInfoService, IStockService stockService,
            IOutStockLockInfoService outStockLockInfoService, IStockInfoDetailService stockInfoDetailService, ILocationInfoService locationInfoService,
            IOutboundOrderDetailService outboundOrderDetailService, ISplitPackageService splitPackageService, IOutboundOrderService outboundOrderService,
            IRepository<Dt_Task> taskRepository, IESSApiService eSSApiService, ILogger<OutboundPickingService> logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService, IAllocateService allocateService) : base(BaseDal)
            IRepository<Dt_Task> taskRepository, IESSApiService eSSApiService, ILogger<OutboundPickingService> logger, IInvokeMESService invokeMESService, IDailySequenceService dailySequenceService, IAllocateService allocateService, IRepository<Dt_OutboundBatch> outboundBatchRepository) : base(BaseDal)
        {
            _unitOfWorkManage = unitOfWorkManage;
            _stockInfoService = stockInfoService;
@@ -78,9 +81,341 @@
            _invokeMESService = invokeMESService;
            _dailySequenceService = dailySequenceService;
            _allocateService = allocateService;
            _outboundBatchRepository = outboundBatchRepository;
        }
        // <summary>
        /// èŽ·å–æ‰˜ç›˜çš„é”å®šä¿¡æ¯
        /// </summary>
        public async Task<List<PalletLockInfoDto>> GetPalletLockInfos(string orderNo, string palletCode)
        {
            var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                  .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
                  .Select(x => new
                  {
                      x.Id,
                      x.OrderNo,
                      x.BatchNo,
                      x.MaterielCode,
                      x.CurrentBarcode,
                      x.AssignQuantity,
                      x.PickedQty,
                      x.Status,
                      x.LocationCode,
                      x.PalletCode
                  }).ToListAsync();
            var lockInfoDtos = lockInfos.Select(x => new PalletLockInfoDto
            {
                Id = x.Id,
                OrderNo = x.OrderNo,
                BatchNo = x.BatchNo,
                MaterielCode = x.MaterielCode,
                CurrentBarcode = x.CurrentBarcode,
                AssignQuantity = x.AssignQuantity,
                PickedQty = x.PickedQty,
                Status = x.Status,
                LocationCode = x.LocationCode,
                PalletCode = x.PalletCode,
                CanSplit = (x.Status == (int)OutLockStockStatusEnum.出库中 && x.AssignQuantity - x.PickedQty > 0),
                CanPick = (x.Status == (int)OutLockStockStatusEnum.出库中 && x.PickedQty < x.AssignQuantity)
            }).ToList();
            return lockInfoDtos;
        }
        #region æŸ¥è¯¢æ–¹æ³•
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çš„å·²æ‹£é€‰åˆ—è¡¨
        /// </summary>
        public async Task<List<PalletPickedInfoDto>> GetPalletPickedList(string orderNo, string palletCode)
        {
            var pickedList = await Db.Queryable<Dt_PickingRecord>()
                .Where(x => x.OrderNo == orderNo &&
                           x.PalletCode == palletCode &&
                           !x.IsCancelled)
                .Select(x => new PalletPickedInfoDto
                {
                    Id = x.Id,
                    OrderNo = x.OrderNo,
                    OrderDetailId = x.OrderDetailId,
                    PalletCode = x.PalletCode,
                    Barcode = x.Barcode,
                    MaterielCode = x.MaterielCode,
                    PickedQty = x.PickQuantity,
                    PickTime = x.PickTime,
                    Operator = x.Operator,
                    LocationCode = x.LocationCode
                })
                .ToListAsync();
            return pickedList;
        }
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çŠ¶æ€
        /// </summary>
        public async Task<PalletStatusDto> GetPalletStatus(string orderNo, string palletCode)
        {
            // èŽ·å–æ‰˜ç›˜çš„é”å®šä¿¡æ¯
            var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
                .ToListAsync();
            if (!lockInfos.Any())
            {
                return new PalletStatusDto
                {
                    OrderNo = orderNo,
                    PalletCode = palletCode,
                    Status = (int)PalletStatusEnum.无任务,
                    StatusText = "无任务",
                    TotalItems = 0,
                    CompletedItems = 0,
                    PendingItems = 0
                };
            }
            var totalItems = lockInfos.Count;
            var completedItems = lockInfos.Count(x => x.Status == (int)OutLockStockStatusEnum.拣选完成);
            var pendingItems = lockInfos.Count(x => x.Status == (int)OutLockStockStatusEnum.出库中);
            var status = PalletStatusEnum.拣选中;
            if (pendingItems == 0 && completedItems > 0)
            {
                status = PalletStatusEnum.已完成;
            }
            else if (pendingItems > 0 && completedItems == 0)
            {
                status = PalletStatusEnum.未开始;
            }
            else if (pendingItems > 0 && completedItems > 0)
            {
                status = PalletStatusEnum.拣选中;
            }
            return new PalletStatusDto
            {
                OrderNo = orderNo,
                PalletCode = palletCode,
                Status = (int)status,
                StatusText = GetPalletStatusText(status),
                TotalItems = totalItems,
                CompletedItems = completedItems,
                PendingItems = pendingItems
            };
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…ä¿¡æ¯
        /// </summary>
        public async Task<SplitPackageInfoDto> GetSplitPackageInfo(string orderNo, string palletCode, string barcode)
        {
            // æŸ¥æ‰¾é”å®šä¿¡æ¯
            var lockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo &&
                           x.PalletCode == palletCode &&
                           x.CurrentBarcode == barcode
                        //&& x.Status == (int)OutLockStockStatusEnum.出库中
                          )
                .FirstAsync();
            if (lockInfo == null)
                throw new Exception("未找到有效的锁定信息");
            // è®¡ç®—剩余可拆数量
            var remainQuantity = lockInfo.AssignQuantity - lockInfo.PickedQty;
            return new SplitPackageInfoDto
            {
                OrderNo = orderNo,
                PalletCode = palletCode,
                Barcode = barcode,
                MaterielCode = lockInfo.MaterielCode,
                RemainQuantity = remainQuantity,
                AssignQuantity = lockInfo.AssignQuantity,
                PickedQty = lockInfo.PickedQty
            };
        }
        #endregion
        #region å–走空箱逻辑
        /// <summary>
        /// å–走空箱 - æ¸…理已完成拣选的托盘数据
        /// </summary>
        public async Task<WebResponseContent> RemoveEmptyPallet(string orderNo, string palletCode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                //  éªŒè¯æ‰˜ç›˜æ˜¯å¦å¯ä»¥å–走(必须全部完成拣选)
                var validationResult = await ValidateEmptyPalletRemoval(orderNo, palletCode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var completedLocks = validationResult.Data;
                // æ¸…理锁定记录(标记为已完成)
                await CleanupCompletedLocks(completedLocks);
                // æ›´æ–°ç›¸å…³è®¢å•状态
                await UpdateOrderStatusAfterPalletRemoval(orderNo);
                //  è®°å½•操作历史
                // await RecordEmptyPalletRemoval(orderNo, palletCode, completedLocks);
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK("取走空箱成功");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取走空箱失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取走空箱失败:{ex.Message}");
            }
        }
        /// <summary>
        /// éªŒè¯ç©ºç®±å–走条件
        /// </summary>
        private async Task<ValidationResult<List<Dt_OutStockLockInfo>>> ValidateEmptyPalletRemoval(string orderNo, string palletCode)
        {
            // èŽ·å–æ‰˜ç›˜çš„æ‰€æœ‰é”å®šè®°å½•
            var lockInfos = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo && x.PalletCode == palletCode)
                .ToListAsync();
            if (!lockInfos.Any())
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error("该托盘没有锁定记录");
            // æ£€æŸ¥æ˜¯å¦æœ‰æœªå®Œæˆçš„锁定记录
            var unfinishedLocks = lockInfos.Where(x =>
                x.Status == (int)OutLockStockStatusEnum.出库中 ||
                x.Status == (int)OutLockStockStatusEnum.回库中).ToList();
            if (unfinishedLocks.Any())
            {
                var unfinishedCount = unfinishedLocks.Count;
                var unfinishedQty = unfinishedLocks.Sum(x => x.AssignQuantity - x.PickedQty);
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error(
                    $"托盘还有{unfinishedCount}条未完成记录,剩余数量{unfinishedQty},不能取走空箱");
            }
            // èŽ·å–å·²å®Œæˆçš„é”å®šè®°å½•
            var completedLocks = lockInfos.Where(x =>
                x.Status == (int)OutLockStockStatusEnum.拣选完成).ToList();
            if (!completedLocks.Any())
                return ValidationResult<List<Dt_OutStockLockInfo>>.Error("该托盘没有已完成拣选的记录");
            return ValidationResult<List<Dt_OutStockLockInfo>>.Success(completedLocks);
        }
        /// <summary>
        /// æ¸…理已完成的锁定记录
        /// </summary>
        private async Task CleanupCompletedLocks(List<Dt_OutStockLockInfo> completedLocks)
        {
            foreach (var lockInfo in completedLocks)
            {
                // æ ‡è®°é”å®šè®°å½•为已取走(可以新增状态或直接删除,根据业务需求)
                // è¿™é‡Œæˆ‘们将其状态更新为"已取走",并记录取走时间
                lockInfo.Status = (int)OutLockStockStatusEnum.已取走;
                lockInfo.Operator = App.User.UserName;
                await _outStockLockInfoService.Db.Updateable(lockInfo).ExecuteCommandAsync();
                // åŒæ—¶æ¸…理对应的库存记录状态
                await CleanupStockInfo(lockInfo);
            }
        }
        /// <summary>
        /// æ¸…理库存信息
        /// </summary>
        private async Task CleanupStockInfo(Dt_OutStockLockInfo lockInfo)
        {
            var stockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == lockInfo.CurrentBarcode && x.StockId == lockInfo.StockId);
            if (stockDetail != null)
            {
                // å¦‚果库存已经出库完成,标记为已清理
                if (stockDetail.Status == (int)StockStatusEmun.出库完成)
                {
                    stockDetail.Status = (int)StockStatusEmun.已清理;
                    await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
                }
            }
        }
        /// <summary>
        /// æ›´æ–°è®¢å•状态
        /// </summary>
        private async Task UpdateOrderStatusAfterPalletRemoval(string orderNo)
        {
            // æ£€æŸ¥è®¢å•是否所有托盘都已完成
            var allLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.OrderNo == orderNo)
                .ToListAsync();
            var unfinishedPallets = allLocks
                .GroupBy(x => x.PalletCode)
                .Where(g => g.Any(x => x.Status == (int)OutLockStockStatusEnum.出库中 ||
                                      x.Status == (int)OutLockStockStatusEnum.回库中))
                .ToList();
            // å¦‚果没有未完成的托盘,更新订单状态为出库完成
            if (!unfinishedPallets.Any())
            {
                await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                    .SetColumns(x => new Dt_OutboundOrder
                    {
                        OrderStatus = (int)OutOrderStatusEnum.出库完成,
                    })
                    .Where(x => x.OrderNo == orderNo)
                    .ExecuteCommandAsync();
            }
        }
        /// <summary>
        /// è®°å½•空箱取走历史
        /// </summary>
        private async Task RecordEmptyPalletRemoval(string orderNo, string palletCode, List<Dt_OutStockLockInfo> completedLocks)
        {
            var removalRecord = new Dt_EmptyPalletRemoval
            {
                OrderNo = orderNo,
                PalletCode = palletCode,
                RemovalTime = DateTime.Now,
                Operator = App.User.UserName,
                CompletedItemsCount = completedLocks.Count,
                TotalPickedQuantity = completedLocks.Sum(x => x.PickedQty)
            };
            await Db.Insertable(removalRecord).ExecuteCommandAsync();
        }
        #endregion
        #region è¾…助方法
        private string GetPalletStatusText(PalletStatusEnum status)
        {
            return status switch
            {
                PalletStatusEnum.未开始 => "未开始",
                PalletStatusEnum.拣选中 => "拣选中",
                PalletStatusEnum.已完成 => "已完成",
                PalletStatusEnum.无任务 => "无任务",
                _ => "未知"
            };
        }
        #endregion
        #region åˆ†æ‰¹åˆ†æ‹£
        /// <summary>
@@ -216,8 +551,10 @@
        #region å–消拆包
        #region å–消拆包 - ä¿®å¤ç‰ˆæœ¬
        /// <summary>
        /// å–消拆包
        /// å–消拆包 - æ”¯æŒå¤šæ¬¡æ‹†åŒ…的情况
        /// </summary>
        public async Task<WebResponseContent> CancelSplitPackage(string orderNo, string palletCode, string newBarcode)
        {
@@ -225,15 +562,29 @@
            {
                _unitOfWorkManage.BeginTran();
                // æŸ¥æ‰¾æ‹†åŒ…记录并验证
                // 1. æŸ¥æ‰¾æ‹†åŒ…记录并验证
                var validationResult = await ValidateCancelSplitRequest(orderNo, palletCode, newBarcode);
                if (!validationResult.IsValid)
                    return WebResponseContent.Instance.Error(validationResult.ErrorMessage);
                var (splitRecord, newLockInfo, newStockDetail) = validationResult.Data;
                // æ‰§è¡Œå–消拆包逻辑
                await ExecuteCancelSplitLogic(splitRecord, newLockInfo, newStockDetail);
                // 2. æŸ¥æ‰¾åŽŸå§‹é”å®šä¿¡æ¯
                var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
                // 3. æ£€æŸ¥è¯¥æ¡ç æ˜¯å¦è¢«å†æ¬¡æ‹†åŒ…
                var childSplitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.OriginalBarcode == newBarcode && !x.IsReverted)
                    .ToListAsync();
                if (childSplitRecords.Any())
                {
                    return WebResponseContent.Instance.Error("该条码已被再次拆包,请先取消后续的拆包操作");
                }
                // 4. æ‰§è¡Œå–消拆包逻辑
                await ExecuteCancelSplitLogic(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
                _unitOfWorkManage.CommitTran();
@@ -242,10 +593,353 @@
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消拆包失败 - OrderNo: {orderNo}, Barcode: {newBarcode}, Error: {ex.Message}");
                _logger.LogError($"取消拆包失败 - OrderNo: {orderNo}, PalletCode: {palletCode}, Barcode: {newBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取消拆包失败:{ex.Message}");
            }
        }
        /// <summary>
        /// æ‰§è¡Œå–消拆包逻辑 - ä¿®å¤ç‰ˆæœ¬
        /// </summary>
        private async Task ExecuteCancelSplitLogic(Dt_SplitPackageRecord splitRecord,
            Dt_OutStockLockInfo originalLockInfo, Dt_OutStockLockInfo newLockInfo,
            Dt_StockInfoDetail newStockDetail)
        {
            // 1. æ¢å¤åŽŸé”å®šä¿¡æ¯
            // æ³¨æ„ï¼šè¿™é‡Œéœ€è¦ç´¯åŠ ï¼Œè€Œä¸æ˜¯ç®€å•çš„èµ‹å€¼ï¼Œå› ä¸ºå¯èƒ½æœ‰å¤šæ¬¡æ‹†åŒ…
            originalLockInfo.AssignQuantity += splitRecord.SplitQty;
            originalLockInfo.OrderQuantity += splitRecord.SplitQty;
            // å¦‚果原锁定信息的状态是拣选完成,需要重新设置为出库中
            if (originalLockInfo.Status == (int)OutLockStockStatusEnum.拣选完成)
            {
                originalLockInfo.Status = (int)OutLockStockStatusEnum.出库中;
            }
            await _outStockLockInfoService.Db.Updateable(originalLockInfo).ExecuteCommandAsync();
            // 2. æ¢å¤åŽŸåº“å­˜æ˜Žç»†
            var originalStock = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == splitRecord.OriginalBarcode && x.StockId == splitRecord.StockId);
            originalStock.StockQuantity += splitRecord.SplitQty;
            // å¦‚果原库存状态是出库完成,需要重新设置为出库锁定
            if (originalStock.Status == (int)StockStatusEmun.出库完成)
            {
                originalStock.Status = (int)StockStatusEmun.出库锁定;
            }
            await _stockInfoDetailService.Db.Updateable(originalStock).ExecuteCommandAsync();
            // 3. åˆ é™¤æ–°é”å®šä¿¡æ¯
            await _outStockLockInfoService.Db.Deleteable<Dt_OutStockLockInfo>()
                .Where(x => x.Id == newLockInfo.Id)
                .ExecuteCommandAsync();
            // 4. åˆ é™¤æ–°åº“存明细
            await _stockInfoDetailService.Db.Deleteable<Dt_StockInfoDetail>()
                .Where(x => x.Barcode == newLockInfo.CurrentBarcode)
                .ExecuteCommandAsync();
            // 5. æ ‡è®°æ‹†åŒ…记录为已撤销
            splitRecord.IsReverted = true;
            splitRecord.RevertTime = DateTime.Now;
            splitRecord.RevertOperator = App.User.UserName;
            await _splitPackageService.Db.Updateable(splitRecord).ExecuteCommandAsync();
            // 6. æ£€æŸ¥å¹¶æ›´æ–°æ‰¹æ¬¡å’Œè®¢å•状态
            await CheckAndUpdateBatchStatus(originalLockInfo.BatchNo);
            await CheckAndUpdateOrderStatus(originalLockInfo.OrderNo);
        }
        /// <summary>
        /// éªŒè¯å–消拆包请求 - å¢žå¼ºç‰ˆæœ¬
        /// </summary>
        private async Task<ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateCancelSplitRequest(
            string orderNo, string palletCode, string newBarcode)
        {
            // æŸ¥æ‰¾æ‹†åŒ…记录
            var splitRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                .Where(x => x.NewBarcode == newBarcode &&
                           x.OrderNo == orderNo &&
                           !x.IsReverted)
                .FirstAsync();
            if (splitRecord == null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到拆包记录");
            // æŸ¥æ‰¾æ–°é”å®šä¿¡æ¯
            var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.CurrentBarcode == newBarcode &&
                           x.PalletCode == palletCode &&
                           x.OrderNo == orderNo)
                .FirstAsync();
            if (newLockInfo == null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到新锁定信息");
            // æ£€æŸ¥æ–°æ¡ç æ˜¯å¦å·²è¢«åˆ†æ‹£
            var pickingRecord = await Db.Queryable<Dt_PickingRecord>()
                .Where(x => x.Barcode == newBarcode && !x.IsCancelled)
                .FirstAsync();
            if (pickingRecord != null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("该条码已被分拣,无法取消拆包");
            // æ£€æŸ¥æ–°æ¡ç æ˜¯å¦è¢«å†æ¬¡æ‹†åŒ…
            var childSplitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                .Where(x => x.OriginalBarcode == newBarcode && !x.IsReverted)
                .ToListAsync();
            if (childSplitRecords.Any())
            {
                var childBarcodes = string.Join(", ", childSplitRecords.Select(x => x.NewBarcode));
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error(
                    $"该条码已被再次拆包,生成的新条码:{childBarcodes},请先取消后续拆包");
            }
            var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == newBarcode);
            return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((splitRecord, newLockInfo, newStockDetail));
        }
        #endregion
        #region æ‰¹é‡å–消拆包链
        /// <summary>
        /// æ‰¹é‡å–消拆包链 - å–消某个条码及其所有后续拆包
        /// </summary>
        public async Task<WebResponseContent> CancelSplitPackageChain(string orderNo, string palletCode, string startBarcode)
        {
            try
            {
                _unitOfWorkManage.BeginTran();
                // 1. æŸ¥æ‰¾æ‰€æœ‰ç›¸å…³çš„æ‹†åŒ…记录(形成拆包链)
                var splitChain = await GetSplitPackageChain(orderNo, startBarcode);
                if (!splitChain.Any())
                    return WebResponseContent.Instance.Error("未找到拆包记录");
                // 2. æŒ‰æ‹†åŒ…顺序倒序取消(从最新的开始取消)
                var reversedChain = splitChain.OrderByDescending(x => x.SplitTime).ToList();
                foreach (var splitRecord in reversedChain)
                {
                    await CancelSingleSplitPackage(splitRecord, palletCode);
                }
                _unitOfWorkManage.CommitTran();
                return WebResponseContent.Instance.OK($"成功取消拆包链,共{reversedChain.Count}次拆包操作");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"取消拆包链失败 - OrderNo: {orderNo}, StartBarcode: {startBarcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"取消拆包链失败:{ex.Message}");
            }
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…é“¾ - æŸ¥æ‰¾æŸä¸ªæ¡ç çš„æ‰€æœ‰æ‹†åŒ…记录(包括后续拆包)
        /// </summary>
        public async Task<List<Dt_SplitPackageRecord>> GetSplitPackageChain(string orderNo, string startBarcode)
        {
            var allSplitRecords = new List<Dt_SplitPackageRecord>();
            var visitedBarcodes = new HashSet<string>(); // é˜²æ­¢å¾ªçŽ¯å¼•ç”¨
            // ä½¿ç”¨é˜Ÿåˆ—进行广度优先搜索
            var queue = new Queue<string>();
            queue.Enqueue(startBarcode);
            visitedBarcodes.Add(startBarcode);
            while (queue.Count > 0)
            {
                var currentBarcode = queue.Dequeue();
                // æŸ¥æ‰¾ä»¥å½“前条码为原条码的所有拆包记录
                var splitRecords = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.OriginalBarcode == currentBarcode &&
                               x.OrderNo == orderNo &&
                               !x.IsReverted)
                    .ToListAsync();
                foreach (var record in splitRecords)
                {
                    // é¿å…é‡å¤å¤„理
                    if (!visitedBarcodes.Contains(record.NewBarcode))
                    {
                        allSplitRecords.Add(record);
                        queue.Enqueue(record.NewBarcode);
                        visitedBarcodes.Add(record.NewBarcode);
                    }
                }
            }
            return allSplitRecords;
        }
        /// <summary>
        /// å–消单个拆包记录
        /// </summary>
        private async Task CancelSingleSplitPackage(Dt_SplitPackageRecord splitRecord, string palletCode)
        {
            // æŸ¥æ‰¾ç›¸å…³æ•°æ®
            var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.CurrentBarcode == splitRecord.NewBarcode &&
                           x.PalletCode == palletCode)
                .FirstAsync();
            var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == splitRecord.NewBarcode);
            var originalLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .FirstAsync(x => x.Id == splitRecord.OutStockLockInfoId);
            // æ‰§è¡Œå–消逻辑
            await ExecuteCancelSplitLogic(splitRecord, originalLockInfo, newLockInfo, newStockDetail);
        }
        #endregion
        #region æ‹†åŒ…信息查询增强
        /// <summary>
        /// èŽ·å–æ‹†åŒ…é“¾ä¿¡æ¯
        /// </summary>
        public async Task<WebResponseContent> GetSplitPackageChainInfo(string orderNo, string barcode)
        {
            try
            {
                var splitChain = await GetSplitPackageChain(orderNo, barcode);
                var chainInfo = new SplitPackageChainInfoDto
                {
                    OriginalBarcode = barcode,
                    TotalSplitTimes = splitChain.Count,
                    SplitChain = splitChain.Select(x => new SplitChainItemDto
                    {
                        SplitTime = x.SplitTime,
                        OriginalBarcode = x.OriginalBarcode,
                        NewBarcode = x.NewBarcode,
                        SplitQuantity = x.SplitQty,
                        Operator = x.Operator,
                        IsReverted = x.IsReverted
                    }).ToList()
                };
                return WebResponseContent.Instance.OK("获取成功", chainInfo);
            }
            catch (Exception ex)
            {
                _logger.LogError($"获取拆包链信息失败 - OrderNo: {orderNo}, Barcode: {barcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error("获取拆包链信息失败");
            }
        }
        /// <summary>
        /// æŸ¥æ‰¾æ ¹æ¡ç 
        /// </summary>
        public async Task<string> FindRootBarcode(string orderNo, string startBarcode)
        {
            var currentBarcode = startBarcode;
            var visited = new HashSet<string>();
            while (!string.IsNullOrEmpty(currentBarcode) && !visited.Contains(currentBarcode))
            {
                visited.Add(currentBarcode);
                // æŸ¥æ‰¾å½“前条码是否是由其他条码拆包而来
                var parentRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                    .Where(x => x.NewBarcode == currentBarcode &&
                               x.OrderNo == orderNo &&
                               !x.IsReverted)
                    .FirstAsync();
                if (parentRecord == null)
                {
                    // æ²¡æœ‰çˆ¶çº§æ‹†åŒ…记录,说明这是根条码
                    return currentBarcode;
                }
                currentBarcode = parentRecord.OriginalBarcode;
            }
            // å¦‚果出现循环引用,返回起始条码
            return startBarcode;
        }
        #endregion
        #region æ›´æ–°æ‰¹æ¬¡çŠ¶æ€æ£€æŸ¥
        /// <summary>
        /// æ£€æŸ¥å¹¶æ›´æ–°æ‰¹æ¬¡çŠ¶æ€
        /// </summary>
        private async Task CheckAndUpdateBatchStatus(string batchNo)
        {
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == batchNo);
            if (batch != null)
            {
                // é‡æ–°è®¡ç®—批次完成数量
                var batchLocks = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                    .Where(x => x.BatchNo == batchNo)
                    .ToListAsync();
                var completedQuantity = batchLocks.Where(x => x.Status == (int)OutLockStockStatusEnum.拣选完成)
                                                 .Sum(x => x.PickedQty);
                batch.CompletedQuantity = completedQuantity;
                // æ›´æ–°æ‰¹æ¬¡çŠ¶æ€
                if (batch.CompletedQuantity >= batch.BatchQuantity)
                {
                    batch.BatchStatus = (int)BatchStatusEnum.已完成;
                }
                else if (batch.CompletedQuantity > 0)
                {
                    batch.BatchStatus = (int)BatchStatusEnum.执行中;
                }
                else
                {
                    batch.BatchStatus = (int)BatchStatusEnum.分配中;
                }
                await _outboundBatchRepository.Db.Updateable(batch).ExecuteCommandAsync();
            }
        }
        #endregion
        #region DTOç±»
        public class SplitPackageChainInfoDto
        {
            public string OriginalBarcode { get; set; }
            public string RootBarcode { get; set; } // æ–°å¢žï¼šæ ¹æ¡ç 
            public int TotalSplitTimes { get; set; }
            public string ChainType { get; set; } // "root" æˆ– "branch"
            public List<SplitChainItemDto> SplitChain { get; set; }
        }
        public class SplitChainItemDto
        {
            public DateTime SplitTime { get; set; }
            public string OriginalBarcode { get; set; }
            public string NewBarcode { get; set; }
            public decimal SplitQuantity { get; set; }
            public string Operator { get; set; }
            public bool IsReverted { get; set; }
        }
        #endregion
        #endregion
@@ -337,11 +1031,11 @@
                    $"库存数量不足,需要:{lockInfo.AssignQuantity},实际:{stockDetail.StockQuantity}");
            var batch = await _outboundBatchRepository.Db.Queryable<Dt_OutboundBatch>()
                .FirstAsync(x => x.BatchNo == lockInfo.BatchNo);
                .FirstAsync(x => x.BatchNo == lockInfo.OutboundBatchNo);
            return ValidationResult<(Dt_OutStockLockInfo, Dt_OutboundOrderDetail, Dt_StockInfoDetail, Dt_OutboundBatch)>.Success((lockInfo, orderDetail, stockDetail, batch));
        }
        private async Task<ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateSplitRequest(
            string orderNo, string palletCode, string originalBarcode, decimal splitQuantity)
        {
@@ -366,42 +1060,7 @@
            return ValidationResult<(Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((lockInfo, stockDetail));
        }
        private async Task<ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>> ValidateCancelSplitRequest(
            string orderNo, string palletCode, string newBarcode)
        {
            var splitRecord = await _splitPackageService.Db.Queryable<Dt_SplitPackageRecord>()
                .Where(x => x.NewBarcode == newBarcode &&
                           x.OrderNo == orderNo &&
                           !x.IsReverted)
                .FirstAsync();
            if (splitRecord == null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到拆包记录");
            var newLockInfo = await _outStockLockInfoService.Db.Queryable<Dt_OutStockLockInfo>()
                .Where(x => x.CurrentBarcode == newBarcode &&
                           x.PalletCode == palletCode &&
                           x.OrderNo == orderNo)
                .FirstAsync();
            if (newLockInfo == null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("未找到新锁定信息");
            // æ£€æŸ¥æ–°æ¡ç æ˜¯å¦å·²è¢«åˆ†æ‹£
            var pickingRecord = await Db.Queryable<Dt_PickingRecord>()
                .Where(x => x.Barcode == newBarcode && !x.IsCancelled)
                .FirstAsync();
            if (pickingRecord != null)
                return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Error("该条码已被分拣,无法取消拆包");
            var newStockDetail = await _stockInfoDetailService.Db.Queryable<Dt_StockInfoDetail>()
                .FirstAsync(x => x.Barcode == newBarcode);
            return ValidationResult<(Dt_SplitPackageRecord, Dt_OutStockLockInfo, Dt_StockInfoDetail)>.Success((splitRecord, newLockInfo, newStockDetail));
        }
        #endregion
        #region æ ¸å¿ƒé€»è¾‘方法
@@ -479,7 +1138,13 @@
                Barcode = newBarcode,
                Status = (int)StockStatusEmun.出库锁定,
                SupplyCode = stockDetail.SupplyCode,
                Unit = stockDetail.Unit
                Unit = stockDetail.Unit,
                BarcodeQty=stockDetail.BarcodeQty,
                BarcodeUnit=stockDetail.BarcodeUnit,
                BusinessType=stockDetail.BusinessType,
                InboundOrderRowNo=stockDetail.InboundOrderRowNo,
            };
            await _stockInfoDetailService.Db.Insertable(newStockDetail).ExecuteCommandAsync();
@@ -487,23 +1152,39 @@
            stockDetail.StockQuantity -= splitQuantity;
            await _stockInfoDetailService.Db.Updateable(stockDetail).ExecuteCommandAsync();
            // åˆ›å»ºæ–°é”å®šä¿¡æ¯
            var newLockInfo = new Dt_OutStockLockInfo
            {
                OrderNo = lockInfo.OrderNo,
                OrderDetailId = lockInfo.OrderDetailId,
                OutboundBatchNo = lockInfo.OutboundBatchNo,
                BatchNo = lockInfo.BatchNo,
                MaterielCode = lockInfo.MaterielCode,
                MaterielName = lockInfo.MaterielName,
                StockId = lockInfo.StockId,
                OrderQuantity = splitQuantity,
                //OriginalQuantity = quantity,
                AssignQuantity = splitQuantity,
                PickedQty = 0,
                LocationCode = lockInfo.LocationCode,
                PalletCode = palletCode,
                PalletCode = lockInfo.PalletCode,
                TaskNum = lockInfo.TaskNum,
                Status = (int)OutLockStockStatusEnum.出库中,
                Unit = lockInfo.Unit,
                SupplyCode = lockInfo.SupplyCode,
                OrderType = lockInfo.OrderType,
                CurrentBarcode = newBarcode,
               // OriginalLockQuantity = quantity,
                IsSplitted = 1,
                ParentLockId = lockInfo.Id,
                Operator = App.User.UserName,
                FactoryArea = lockInfo.FactoryArea,
                lineNo = lockInfo.lineNo,
                WarehouseCode = lockInfo.WarehouseCode,
                BarcodeQty = lockInfo.BarcodeQty,
                BarcodeUnit = lockInfo.BarcodeUnit,
            };
            await _outStockLockInfoService.Db.Insertable(newLockInfo).ExecuteCommandAsync();
            // æ›´æ–°åŽŸé”å®šä¿¡æ¯
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -95,7 +95,7 @@
        public List<int> TaskOutboundTypes => typeof(TaskTypeEnum).GetEnumIndexList();
        public TaskService(IRepository<Dt_Task> BaseDal, IMapper mapper, IUnitOfWorkManage unitOfWorkManage, IRepository<Dt_StockInfo> stockRepository, ILocationInfoService locationInfoService, IInboundOrderService inboundOrderService, ILocationStatusChangeRecordService locationStatusChangeRecordService, IESSApiService eSSApiService, ILogger<TaskService> logger, IStockService stockService, IRecordService recordService, IInboundOrderDetailService inboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutboundOrderDetailService outboundOrderDetailService, IInvokeMESService invokeMESService, IOutStockLockInfoService outStockLockInfoService, IAllocateService allocateService) : base(BaseDal)
        public TaskService(IRepository<Dt_Task> BaseDal, IMapper mapper, IUnitOfWorkManage unitOfWorkManage, IRepository<Dt_StockInfo> stockRepository, ILocationInfoService locationInfoService, IInboundOrderService inboundOrderService, ILocationStatusChangeRecordService locationStatusChangeRecordService, IESSApiService eSSApiService, ILogger<TaskService> logger, IStockService stockService, IRecordService recordService, IInboundOrderDetailService inboundOrderDetailService, IOutboundOrderService outboundOrderService, IOutboundOrderDetailService outboundOrderDetailService, IInvokeMESService invokeMESService, IOutStockLockInfoService outStockLockInfoService, IAllocateService allocateService, IRepository<Dt_OutboundBatch> outboundBatchRepository) : base(BaseDal)
        {
            _mapper = mapper;
            _unitOfWorkManage = unitOfWorkManage;
@@ -113,6 +113,7 @@
            _invokeMESService = invokeMESService;
            _outStockLockInfoService = outStockLockInfoService;
            _allocateService = allocateService;
            _OutboundBatchRepository = outboundBatchRepository;
        }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_Outbound.cs
@@ -637,7 +637,7 @@
        /// <summary>
        /// åˆ†æ‰¹åˆ†é…åº“å­˜
        /// </summary>
        public async Task<WebResponseContent> BatchAllocateStock(string orderNo, int orderDetailId, decimal batchQuantity, string outStation)
        public async Task<WebResponseContent> GenerateOutboundBatchTasksAsync(int orderDetailId, decimal batchQuantity, string outStation)
        {
            try
            {
@@ -647,7 +647,7 @@
                List<Dt_OutStockLockInfo> outStockLockInfos = new List<Dt_OutStockLockInfo>();
                List<Dt_LocationInfo> locationInfos = new List<Dt_LocationInfo>();
                (List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) result = await BatchAllocateStockDataHandle(orderNo, orderDetailId, batchQuantity, outStation);
                (List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?) result = await BatchAllocateStockDataHandle(orderDetailId, batchQuantity, outStation);
                if (result.Item2 != null && result.Item2.Count > 0)
                {
@@ -676,7 +676,7 @@
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                _logger.LogError($"分批分配库存失败 - OrderNo: {orderNo}, OrderDetailId: {orderDetailId}, Quantity: {batchQuantity}, Error: {ex.Message}");
                _logger.LogError($"分批分配库存失败 -  OrderDetailId: {orderDetailId}, Quantity: {batchQuantity}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error($"分批分配失败:{ex.Message}");
            }
        }
@@ -685,7 +685,7 @@
        /// åˆ†æ‰¹åˆ†é…åº“存数据处理
        /// </summary>
        public async Task<(List<Dt_Task>, List<Dt_StockInfo>?, List<Dt_OutboundOrderDetail>?, List<Dt_OutStockLockInfo>?, List<Dt_LocationInfo>?)>
            BatchAllocateStockDataHandle(string orderNo, int orderDetailId, decimal batchQuantity, string outStation)
            BatchAllocateStockDataHandle( int orderDetailId, decimal batchQuantity, string outStation)
        {
            List<Dt_Task> tasks = new List<Dt_Task>();
@@ -697,7 +697,11 @@
            {
                throw new Exception("未找到出库单明细信息");
            }
            var  outboundOrder = await _outboundOrderService.Db.Queryable<Dt_OutboundOrder>().FirstAsync(x => x.Id == outboundOrderDetail.OrderId);
            if(outboundOrder == null)
            {
                throw new Exception("未找到出库单信息");
            }
            // éªŒè¯è®¢å•明细状态
            if (outboundOrderDetail.OrderDetailStatus > OrderDetailStatusEnum.New.ObjToInt() &&
                outboundOrderDetail.OrderDetailStatus != OrderDetailStatusEnum.AssignOverPartial.ObjToInt())
@@ -732,9 +736,8 @@
            if (allocateResult.Item1 != null && allocateResult.Item1.Count > 0)
            {
                // åˆ›å»ºåˆ†æ‰¹è®°å½•
                await CreateBatchRecord(orderNo, orderDetailId, batchQuantity, batchNo);
                Dt_OutboundOrder outboundOrder = await _outboundOrderService.Repository.QueryFirstAsync(x => x.Id == outboundOrderDetail.OrderId);
                await CreateBatchRecord(outboundOrder.OrderNo, orderDetailId, batchQuantity, batchNo);
                TaskTypeEnum typeEnum = outboundOrder.OrderType switch
                {
                    (int)OutOrderTypeEnum.Issue => TaskTypeEnum.Outbound,
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Outbound/OutboundBatchPickingController.cs
@@ -1,7 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using WIDESEA_Core;
using WIDESEA_Core.BaseController;
using WIDESEA_DTO.Outbound;
using WIDESEA_IOutboundService;
using WIDESEA_Model.Models;
using static WIDESEA_OutboundService.OutboundBatchPickingService;
namespace WIDESEA_WMSServer.Controllers.Outbound
{
@@ -12,10 +15,203 @@
    {
        private readonly ISplitPackageService _splitPackageService;
        private readonly IOutStockLockInfoService _outStockLockInfoService;
        public OutboundBatchPickingController(IOutboundBatchPickingService service, ISplitPackageService splitPackageService, IOutStockLockInfoService outStockLockInfoService) : base(service)
        private readonly IOutboundBatchPickingService _outboundBatchPickingService;
        private readonly ILogger<OutboundBatchPickingController> _logger;
        public OutboundBatchPickingController(IOutboundBatchPickingService service, ISplitPackageService splitPackageService, IOutStockLockInfoService outStockLockInfoService, IOutboundBatchPickingService outboundBatchPickingService, ILogger<OutboundBatchPickingController> logger) : base(service)
        {
            _splitPackageService = splitPackageService;
            _outStockLockInfoService = outStockLockInfoService;
            _outboundBatchPickingService = outboundBatchPickingService;
            _logger = logger;
        }
        /// <summary>
        /// åˆ†æ‹£ç¡®è®¤
        /// </summary>
        [HttpPost("confirm-picking")]
        public async Task<WebResponseContent> ConfirmPicking([FromBody] ConfirmPickingDto dto)
        {
            return await _outboundBatchPickingService.ConfirmBatchPicking(dto.OrderNo, dto.PalletCode, dto.Barcode);
        }
        /// <summary>
        /// å–消分拣
        /// </summary>
        [HttpPost("cancel-picking")]
        public async Task<WebResponseContent> CancelPicking([FromBody] CancelPickingDto dto)
        {
            return await _outboundBatchPickingService.CancelPicking(dto.OrderNo, dto.PalletCode, dto.Barcode);
        }
        /// <summary>
        /// å–消拆包链
        /// </summary>
        [HttpPost("cancel-split-chain")]
        public async Task<WebResponseContent> CancelSplitChain([FromBody] CancelSplitChainDto dto)
        {
            return await _outboundBatchPickingService.CancelSplitPackageChain(dto.OrderNo, dto.PalletCode, dto.StartBarcode);
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…é“¾ä¿¡æ¯
        /// </summary>
        [HttpPost("split-package-chain-info")]
        public async Task<WebResponseContent> GetSplitPackageChainInfo([FromBody] SplitPackageChainInfoRequestDto dto)
        {
            return await _outboundBatchPickingService.GetSplitPackageChainInfo(dto.OrderNo, dto.Barcode);
        }
        /// <summary>
        /// æŸ¥æ‰¾å®Œæ•´æ‹†åŒ…链(从根条码开始)
        /// </summary>
        [HttpPost("find-root-split-chain")]
        public async Task<WebResponseContent> FindRootSplitChain([FromBody] SplitPackageChainInfoRequestDto dto)
        {
            try
            {
                // æŸ¥æ‰¾æ ¹æ¡ç 
                var rootBarcode = await _outboundBatchPickingService. FindRootBarcode(dto.OrderNo, dto.Barcode);
                // èŽ·å–å®Œæ•´æ‹†åŒ…é“¾
                var splitChain = await _outboundBatchPickingService.GetSplitPackageChain(dto.OrderNo, rootBarcode);
                var chainInfo = new SplitPackageChainInfoDto
                {
                    OriginalBarcode = rootBarcode,
                    RootBarcode = rootBarcode,
                    TotalSplitTimes = splitChain.Count,
                    ChainType = "root",
                    SplitChain = splitChain.Select(x => new SplitChainItemDto
                    {
                        SplitTime = x.SplitTime,
                        OriginalBarcode = x.OriginalBarcode,
                        NewBarcode = x.NewBarcode,
                        SplitQuantity = x.SplitQty,
                        Operator = x.Operator,
                        IsReverted = x.IsReverted
                    }).ToList()
                };
                return WebResponseContent.Instance.OK("获取成功", chainInfo);
            }
            catch (Exception ex)
            {
                _logger.LogError($"查找完整拆包链失败 - OrderNo: {dto.OrderNo}, Barcode: {dto.Barcode}, Error: {ex.Message}");
                return WebResponseContent.Instance.Error("查找完整拆包链失败");
            }
        }
        /// <summary>
        /// æ‰‹åŠ¨æ‹†åŒ…
        /// </summary>
        [HttpPost("split-package")]
        public async Task<WebResponseContent> SplitPackage([FromBody] SplitPackageDto dto)
        {
            return await _outboundBatchPickingService.ManualSplitPackage(dto.OrderNo, dto.PalletCode, dto.OriginalBarcode, dto.SplitQuantity);
        }
        /// <summary>
        /// å–消拆包
        /// </summary>
        [HttpPost("cancel-split")]
        public async Task<WebResponseContent> CancelSplit([FromBody] CancelSplitDto dto)
        {
            return await _outboundBatchPickingService.CancelSplitPackage(dto.OrderNo, dto.PalletCode, dto.NewBarcode);
        }
        /// <summary>
        /// åˆ†æ‰¹å›žåº“
        /// </summary>
        [HttpPost("return-stock")]
        public async Task<WebResponseContent> ReturnStock([FromBody] ReturnStockDto dto)
        {
            return await _outboundBatchPickingService.BatchReturnStock(dto.OrderNo, dto.PalletCode);
        }
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çš„é”å®šä¿¡æ¯
        /// </summary>
        [HttpPost("pallet-locks")]
        public async Task<WebResponseContent> GetPalletLocks([FromBody] PalletLocksDto dto)
        {
            try
            {
                var locks = await _outboundBatchPickingService.GetPalletLockInfos(dto.OrderNo, dto.PalletCode);
                return WebResponseContent.Instance.OK("获取成功", locks);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取托盘锁定信息异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" + ex.Message);
            }
        }
        /// <summary>
        /// èŽ·å–å·²æ‹£é€‰åˆ—è¡¨
        /// </summary>
        [HttpPost("pallet-picked-list")]
        public async Task<WebResponseContent> GetPalletPickedList([FromBody] PalletLocksDto dto)
        {
            try
            {
                var pickedList = await _outboundBatchPickingService.GetPalletPickedList(dto.OrderNo, dto.PalletCode);
                return WebResponseContent.Instance.OK("获取成功", pickedList);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取已拣选列表异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" + ex.Message);
            }
        }
        /// <summary>
        /// èŽ·å–æ‰˜ç›˜çŠ¶æ€
        /// </summary>
        [HttpPost("pallet-status")]
        public async Task<WebResponseContent> GetPalletStatus([FromBody] PalletLocksDto dto)
        {
            try
            {
                var status = await _outboundBatchPickingService.GetPalletStatus(dto.OrderNo, dto.PalletCode);
                return WebResponseContent.Instance.OK("获取成功", status);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取托盘状态异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" + ex.Message);
            }
        }
        /// <summary>
        /// èŽ·å–æ‹†åŒ…ä¿¡æ¯
        /// </summary>
        [HttpPost("split-package-info")]
        public async Task<WebResponseContent> GetSplitPackageInfo([FromBody] SplitPackageInfoDto dto)
        {
            try
            {
                var info = await _outboundBatchPickingService.GetSplitPackageInfo(dto.OrderNo, dto.PalletCode, dto.Barcode);
                return WebResponseContent.Instance.OK("获取成功", info);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"获取拆包信息异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}, Barcode: {dto.Barcode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" +ex.Message);
            }
        }
        /// <summary>
        /// å–走空箱
        /// </summary>
        [HttpPost("remove-empty-pallet")]
        public async Task<WebResponseContent> RemoveEmptyPallet([FromBody] RemoveEmptyPalletDto dto)
        {
            try
            {
                var result = await _outboundBatchPickingService.RemoveEmptyPallet(dto.OrderNo, dto.PalletCode);
                return result;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"取走空箱异常 - OrderNo: {dto.OrderNo}, PalletCode: {dto.PalletCode}");
                return WebResponseContent.Instance.Error("系统异常,请稍后重试" +ex.Message);
            }
        }
    }
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/TaskInfo/TaskController.cs
@@ -88,5 +88,17 @@
            return Service.GenerateOutboundTask(orderDetailId, stockSelectViews);
        }
        /// <summary>
        /// åˆ†æ‰¹ç”Ÿæˆå‡ºåº“任务
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        [HttpPost, HttpGet, Route("GenerateOutboundBatchTasks"), AllowAnonymous]
        public async Task<WebResponseContent> GenerateOutboundBatchTasks([FromBody] GenerateOutboundBatchTasksDto data)
        {
            return await Service.GenerateOutboundBatchTasksAsync(data.orderDetailId,data.batchQuantity, data.outboundPlatform);
        }
    }
}