wanshenmean
9 小时以前 0fa137570bf7ac2bf58c8af2828cd595625fa400
Merge branch 'dev' of http://115.159.85.185:8098/r/SuZhouGuanHong/ShanMeiXinNengYuan into dev
已修改6个文件
2609 ■■■■ 文件已修改
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue 409 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/Index.vue 159 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Extensions/WebSocketSetup.cs 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_AGV.cs 1347 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs 645 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
Code/WMS/WIDESEA_WMSClient/src/views/Home.vue
@@ -3,44 +3,21 @@
    <!-- 顶部:本月出入库趋势 (全宽) -->
    <div class="chart-row full-width">
      <div class="chart-card">
        <div class="card-title">本月出入库趋势</div>
        <div class="card-title">每月出入库趋势</div>
        <div id="chart-monthly-trend" class="chart-content"></div>
      </div>
    </div>
    <!-- 第二行:今日/本周出入库对比 -->
    <div class="chart-row">
    <!-- 第二行:每日出入库趋势 (全宽) -->
    <div class="chart-row full-width">
      <div class="chart-card">
        <div class="card-title">今日出入库对比</div>
        <div id="chart-today" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">本周出入库对比</div>
        <div id="chart-week" class="chart-content"></div>
        <div class="card-title">每日出入库趋势</div>
        <div id="chart-daily" class="chart-content"></div>
      </div>
    </div>
    <!-- 第三行:本月对比/库存总量 -->
    <!-- 第四行:仓库分布 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">本月出入库对比</div>
        <div id="chart-month" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">当前库存总量</div>
        <div class="stock-total">
          <div class="total-number">{{ overviewData.TotalStock || 0 }}</div>
          <div class="total-label">托盘</div>
        </div>
      </div>
    </div>
    <!-- 第四行:库龄分布/仓库分布 -->
    <div class="chart-row">
      <div class="chart-card">
        <div class="card-title">库存库龄分布</div>
        <div id="chart-stock-age" class="chart-content"></div>
      </div>
      <div class="chart-card">
        <div class="card-title">各仓库库存分布</div>
        <div id="chart-warehouse" class="chart-content"></div>
@@ -57,16 +34,8 @@
  data() {
    return {
      charts: {},
      overviewData: {
        TodayInbound: 0,
        TodayOutbound: 0,
        MonthInbound: 0,
        MonthOutbound: 0,
        TotalStock: 0
      },
      weeklyData: [],
      dailyData: [],
      monthlyData: [],
      stockAgeData: [],
      warehouseData: []
    };
  },
@@ -86,53 +55,22 @@
    initCharts() {
      this.charts.monthlyTrend = echarts.init(document.getElementById("chart-monthly-trend"));
      this.charts.today = echarts.init(document.getElementById("chart-today"));
      this.charts.week = echarts.init(document.getElementById("chart-week"));
      this.charts.month = echarts.init(document.getElementById("chart-month"));
      this.charts.stockAge = echarts.init(document.getElementById("chart-stock-age"));
      this.charts.daily = echarts.init(document.getElementById("chart-daily"));
      this.charts.warehouse = echarts.init(document.getElementById("chart-warehouse"));
    },
    async loadData() {
      await this.loadOverview();
      await this.loadWeeklyStats();
      await this.loadMonthlyStats();
      await this.loadStockAgeDistribution();
      await this.loadDailyStats();
      await this.loadStockByWarehouse();
    },
    async loadOverview() {
      try {
        const res = await this.http.get("/api/Dashboard/Overview");
        console.log("总览数据", res.Data);
        if (res.Status && res.Data) {
          this.overviewData = res.Data;
          this.updateTodayChart();
          this.updateWeekChart();
          this.updateMonthChart();
        }
      } catch (e) {
        console.error("加载总览数据失败", e);
      }
    },
    async loadWeeklyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/WeeklyStats", { weeks: 12 });
        if (res.Status && res.Data) {
          this.weeklyData = res.Data;
          this.updateWeekChart();
        }
      } catch (e) {
        console.error("加载每周统计失败", e);
      }
    },
    async loadMonthlyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/MonthlyStats", { months: 12 });
        if (res.Status && res.Data) {
          this.monthlyData = res.Data;
        if (res.status && res.data) {
          console.log("每月统计数据:", res.data);
          this.monthlyData = res.data;
          this.updateMonthlyTrendChart();
        }
      } catch (e) {
@@ -140,110 +78,30 @@
      }
    },
    async loadStockAgeDistribution() {
    async loadDailyStats() {
      try {
        const res = await this.http.get("/api/Dashboard/StockAgeDistribution");
        if (res.Status && res.Data) {
          this.stockAgeData = res.Data;
          this.updateStockAgeChart();
        const res = await this.http.get("/api/Dashboard/DailyStats", { days: 30 });
        if (res.status && res.data) {
          console.log("每日统计数据:", res.data);
          this.dailyData = res.data;
          this.updateDailyChart();
        }
      } catch (e) {
        console.error("加载库龄分布失败", e);
        console.error("加载每日统计失败", e);
      }
    },
    async loadStockByWarehouse() {
      try {
        const res = await this.http.get("/api/Dashboard/StockByWarehouse");
        if (res.Status && res.Data) {
          this.warehouseData = res.Data;
        if (res.status && res.data) {
          console.log("仓库分布数据:", res.data);
          this.warehouseData = res.data.data || res.data;
          this.updateWarehouseChart();
        }
      } catch (e) {
        console.error("加载仓库分布失败", e);
      }
    },
    updateTodayChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["今日"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.TodayInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.TodayOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.today.setOption(option, true);
    },
    updateWeekChart() {
      const thisWeek = this.getThisWeekData(this.weeklyData);
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本周"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [thisWeek.Inbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [thisWeek.Outbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.week.setOption(option, true);
    },
    getThisWeekData(weeklyData) {
      if (!weeklyData || weeklyData.length === 0) return { Inbound: 0, Outbound: 0 };
      const thisWeekKey = this.getCurrentWeekKey();
      const thisWeek = weeklyData.find(w => w.Week === thisWeekKey);
      return thisWeek || { Inbound: 0, Outbound: 0 };
    },
    getCurrentWeekKey() {
      const now = new Date();
      const diff = (7 + (now.getDay() - 1)) % 7;
      const monday = new Date(now);
      monday.setDate(now.getDate() - diff);
      const year = monday.getFullYear();
      const jan1 = new Date(year, 0, 1);
      const weekNum = Math.ceil(((monday - jan1) / 86400000 + jan1.getDay() + 1) / 7);
      return `${year}-W${String(weekNum).padStart(2, "0")}`;
    },
    updateMonthChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: ["本月"],
          axisLabel: { color: "#fff" }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        series: [
          { name: "入库", type: "bar", data: [this.overviewData.MonthInbound], itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: [this.overviewData.MonthOutbound], itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.month.setOption(option, true);
    },
    updateMonthlyTrendChart() {
@@ -252,7 +110,7 @@
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.monthlyData.map(m => m.Month),
          data: this.monthlyData.map(m => m.month),
          axisLabel: { color: "#fff", rotate: 45 }
        },
        yAxis: [
@@ -263,56 +121,144 @@
          }
        ],
        series: [
          { name: "入库", type: "bar", data: this.monthlyData.map(m => m.Inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "line", data: this.monthlyData.map(m => m.Outbound), itemStyle: { color: "#91cc75" } }
          { name: "入库", type: "bar", data: this.monthlyData.map(m => m.inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "line", data: this.monthlyData.map(m => m.outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.monthlyTrend.setOption(option, true);
    },
    updateStockAgeChart() {
    updateDailyChart() {
      const option = {
        tooltip: { trigger: "axis" },
        legend: { data: ["入库", "出库"], textStyle: { color: "#fff" } },
        xAxis: {
          type: "category",
          data: this.stockAgeData.map(s => s.Range),
          axisLabel: { color: "#fff" }
          data: this.dailyData.map(d => d.date),
          axisLabel: {
            color: "#fff",
            interval: 0,
            rotate: 45,
            fontSize: 12,
            margin: 10
          },
          axisTick: {
            alignWithLabel: true
          }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
        },
        grid: {
          left: '3%',
          right: '4%',
          bottom: '15%',
          top: '10%',
          containLabel: true
        },
        series: [
          {
            type: "bar",
            data: this.stockAgeData.map(s => s.Count),
            itemStyle: { color: "#5470c6" }
          }
          { name: "入库", type: "bar", data: this.dailyData.map(d => d.inbound), itemStyle: { color: "#5470c6" } },
          { name: "出库", type: "bar", data: this.dailyData.map(d => d.outbound), itemStyle: { color: "#91cc75" } }
        ]
      };
      this.charts.stockAge.setOption(option, true);
      this.charts.daily.setOption(option, true);
    },
    updateWarehouseChart() {
      const warehouseNames = this.warehouseData.map(w => w.warehouse);
      const totalStocks = this.warehouseData.map(w => w.total);
      const hasStocks = this.warehouseData.map(w => w.hasStock);
      const noStocks = this.warehouseData.map(w => w.noStock);
      const hasStockPercentages = this.warehouseData.map(w => w.hasStockPercentage);
      const noStockPercentages = this.warehouseData.map(w => w.noStockPercentage);
      const option = {
        tooltip: { trigger: "axis" },
        tooltip: {
          trigger: 'axis',
          axisPointer: {
            type: 'shadow'
          },
          formatter: function(params) {
            let tip = params[0].name + '<br/>';
            params.forEach(param => {
              const dataIndex = param.dataIndex;
              const warehouse = window.homeComponent.warehouseData[dataIndex];
              if (param.seriesName === '已用容量') {
                tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.hasStockPercentage})<br/>`;
                tip += `有库存: ${warehouse.hasStock}<br/>`;
                tip += `无库存: ${warehouse.noStock}<br/>`;
                tip += `总容量: ${warehouse.total}`;
              } else if (param.seriesName === '剩余容量') {
                tip += `${param.marker}${param.seriesName}: ${param.value} (${warehouse.noStockPercentage})<br/>`;
                tip += `有库存: ${warehouse.hasStock}<br/>`;
                tip += `无库存: ${warehouse.noStock}<br/>`;
                tip += `总容量: ${warehouse.total}`;
              }
            });
            return tip;
          }
        },
        legend: {
          data: ['已用容量', '剩余容量'],
          textStyle: { color: '#fff' }
        },
        xAxis: {
          type: "category",
          data: this.warehouseData.map(w => w.Warehouse),
          axisLabel: { color: "#fff", rotate: 30 }
          type: 'category',
          data: warehouseNames,
          axisLabel: { color: '#fff', rotate: 30 }
        },
        yAxis: {
          type: "value",
          axisLabel: { color: "#fff" }
          type: 'value',
          axisLabel: { color: '#fff' }
        },
        series: [
          {
            type: "bar",
            data: this.warehouseData.map(w => w.Count),
            itemStyle: { color: "#5470c6" }
            name: '已用容量',
            type: 'bar',
            data: hasStocks.map((value, index) => ({
              value: value,
              label: {
                show: true,
                position: 'top',
                formatter: '{c} {a|' + hasStockPercentages[index] + '}',
                rich: {
                  a: {
                    lineHeight: 20,
                    borderColor: '#91cc75',
                    color: '#91cc75'
                  }
                }
              }
            })),
            itemStyle: { color: '#91cc75' }
          },
          {
            name: '剩余容量',
            type: 'bar',
            data: noStocks.map((value, index) => ({
              value: value,
              label: {
                show: true,
                position: 'top',
                formatter: '{c} {a|' + noStockPercentages[index] + '}',
                rich: {
                  a: {
                    lineHeight: 20,
                    borderColor: '#fac858',
                    color: '#fac858'
                  }
                }
              }
            })),
            itemStyle: { color: '#fac858' }
          }
        ]
      };
      window.homeComponent = this;
      this.charts.warehouse.setOption(option, true);
    }
  }
@@ -322,8 +268,10 @@
<style scoped>
.dashboard-container {
  padding: 20px;
  background-color: #0e1a2b;
  color: #e0e0e0;
  min-height: calc(100vh - 60px);
  background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
  background-attachment: fixed;
}
.chart-row {
@@ -338,11 +286,25 @@
.chart-card {
  flex: 1;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(25, 186, 139, 0.17);
  border-radius: 4px;
  background: rgba(10, 16, 35, 0.6);
  backdrop-filter: blur(10px);
  border: 1px solid rgba(64, 224, 208, 0.3);
  border-radius: 12px;
  padding: 15px;
  position: relative;
  box-shadow:
    0 0 15px rgba(0, 255, 255, 0.1),
    inset 0 0 10px rgba(64, 224, 208, 0.1);
  transition: all 0.3s ease;
  overflow: hidden;
}
.chart-card:hover {
  transform: translateY(-5px);
  box-shadow:
    0 0 25px rgba(0, 255, 255, 0.3),
    inset 0 0 15px rgba(64, 224, 208, 0.2);
  border: 1px solid rgba(64, 224, 208, 0.6);
}
.chart-card::before {
@@ -352,8 +314,9 @@
  left: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-left: 2px solid #02a6b5;
  border-top: 2px solid #00ffff;
  border-left: 2px solid #00ffff;
  box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7);
}
.chart-card::after {
@@ -363,41 +326,39 @@
  right: 0;
  width: 10px;
  height: 10px;
  border-top: 2px solid #02a6b5;
  border-right: 2px solid #02a6b5;
  border-top: 2px solid #00ffff;
  border-right: 2px solid #00ffff;
  box-shadow: 2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7);
}
.chart-card::before,
.chart-card::after {
  animation: neon-flicker 2s infinite alternate;
}
@keyframes neon-flicker {
  0%, 100% {
    opacity: 1;
    box-shadow: -2px -2px 10px #00ffff, 0 0 10px rgba(0, 255, 255, 0.7);
  }
  50% {
    opacity: 0.8;
    box-shadow: -2px -2px 5px #00ffff, 0 0 5px rgba(0, 255, 255, 0.5);
  }
}
.card-title {
  color: #fff;
  color: #00ffff;
  font-size: 16px;
  text-align: center;
  margin-bottom: 10px;
  text-shadow: 0 0 10px rgba(0, 255, 255, 0.7);
  font-weight: 500;
}
.chart-content {
  height: 280px;
  width: 100%;
}
.stock-total {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 280px;
}
.total-number {
  font-size: 64px;
  font-weight: bold;
  color: #67caca;
  font-family: -apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, Helvetica Neue, Helvetica, Arial, sans-serif;
}
.total-label {
  font-size: 18px;
  color: #fcf0d8;
  margin-top: 10px;
}
/* 全宽图表 */
@@ -409,4 +370,20 @@
.full-width .chart-content {
  height: 350px;
}
</style>
/* 添加网格线效果 */
.dashboard-container::before {
  content: "";
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-image:
    linear-gradient(rgba(64, 224, 208, 0.05) 1px, transparent 1px),
    linear-gradient(90deg, rgba(64, 224, 208, 0.05) 1px, transparent 1px);
  background-size: 30px 30px;
  pointer-events: none;
  z-index: -1;
}
</style>
Code/WMS/WIDESEA_WMSClient/src/views/Index.vue
@@ -3,22 +3,12 @@
    <div class="vol-aside" :style="{ width: menuWidth + 'px' }">
      <div class="header" :style="{ width: menuWidth - 1 + 'px' }">
        <img v-show="!isCollapse" v-bind:src="logo" />
        <i
          @click="toggleLeft"
          class="collapse-menu"
          :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'"
        />
        <i @click="toggleLeft" class="collapse-menu" :class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'" />
      </div>
      <div class="vol-menu">
        <el-scrollbar style="height: 100%">
          <VolMenu
            :currentMenuId="currentMenuId"
            :on-select="onSelect"
            :enable="true"
            :open-select="false"
            :isCollapse="isCollapse"
            :list="menuOptions"
          ></VolMenu>
          <VolMenu :currentMenuId="currentMenuId" :on-select="onSelect" :enable="true" :open-select="false"
            :isCollapse="isCollapse" :list="menuOptions"></VolMenu>
        </el-scrollbar>
      </div>
    </div>
@@ -27,14 +17,9 @@
        <div class="project-name">WMS</div>
        <div class="header-text">
          <div class="h-link">
            <a
              href="javascript:void(0)"
              @click="to(item)"
              v-for="(item, index) in links.filter((c) => {
                return !c.icon;
              })"
              :key="index"
            >
            <a href="javascript:void(0)" @click="to(item)" v-for="(item, index) in links.filter((c) => {
              return !c.icon;
            })" :key="index">
              <span v-if="!item.icon"> {{ item.text }}</span>
              <i v-else :class="item.icon"></i>
            </a>
@@ -42,14 +27,9 @@
        </div>
        <div class="header-info">
          <div class="h-link">
            <a
              href="javascript:void(0)"
              @click="to(item)"
              v-for="(item, index) in links.filter((c) => {
                return c.icon;
              })"
              :key="index"
            >
            <a href="javascript:void(0)" @click="to(item)" v-for="(item, index) in links.filter((c) => {
              return c.icon;
            })" :key="index">
              <span v-if="!item.icon"> {{ item.text }}</span>
              <i v-else :class="item.icon"></i>
            </a>
@@ -57,15 +37,9 @@
          <!--消息管理-->
          <div class="h-link" @click="messageModel = true">
            <a
              ><i class="el-icon-message-solid"
                ><el-badge
                  :value="messageList.length"
                  :type="messageList.length > 0 ? 'danger' : 'success'"
                  class="item"
                  style="width: 10px"
                ></el-badge></i
            ></a>
            <a><i class="el-icon-message-solid"><el-badge :value="messageList.length"
                  :type="messageList.length > 0 ? 'danger' : 'success'" class="item"
                  style="width: 10px"></el-badge></i></a>
          </div>
          <div>
            <img class="user-header" :src="userImg" :onerror="errorImg" />
@@ -75,62 +49,37 @@
            <span id="index-date"></span>
          </div>
          <div class="settings">
            <i
              style="font-size: 20px"
              class="el-icon-s-tools"
              @click="drawer_model = true"
            />
            <i style="font-size: 20px" class="el-icon-s-tools" @click="drawer_model = true" />
          </div>
        </div>
      </div>
      <div class="vol-path">
        <el-tabs
          @tab-click="selectNav"
          @tab-remove="removeNav"
          @contextmenu.prevent="bindRightClickMenu(false)"
          type="border-card"
          class="header-navigation"
          v-model="selectId"
          :strtch="false"
        >
          <el-tab-pane
            v-for="(item, navIndex) in navigation"
            type="card"
            :name="navIndex + ''"
            :closable="navIndex > 0"
            :key="navIndex"
            :label="item.name"
          >
        <el-tabs @tab-click="selectNav" @tab-remove="removeNav" @contextmenu.prevent="bindRightClickMenu(false)"
          type="border-card" class="header-navigation" v-model="selectId" :strtch="false">
          <el-tab-pane v-for="(item, navIndex) in navigation" type="card" :name="navIndex + ''" :closable="navIndex > 0"
            :key="navIndex" :label="item.name">
            <span style="display: none">{{ navIndex }}</span>
          </el-tab-pane>
        </el-tabs>
        <!-- 右键菜单 -->
        <div v-show="contextMenuVisible">
          <ul
            :style="{ left: menuLeft + 'px', top: menuTop + 'px' }"
            class="contextMenu"
          >
          <ul :style="{ left: menuLeft + 'px', top: menuTop + 'px' }" class="contextMenu">
            <li v-show="visibleItem.all">
              <el-button link @click="closeTabs()">
                <i class="el-icon-close"></i>
                {{
                  navigation.length == 2 ? "关闭菜单" : "关闭所有"
                }}</el-button
              >
                }}</el-button>
            </li>
            <li v-show="visibleItem.left">
              <el-button link @click="closeTabs('left')"
                ><i class="el-icon-back"></i>关闭左边</el-button
              >
              <el-button link @click="closeTabs('left')"><i class="el-icon-back"></i>关闭左边</el-button>
            </li>
            <li v-show="visibleItem.right">
              <el-button link @click="closeTabs('right')">
                <i class="el-icon-right"></i>关闭右边</el-button
              >
                <i class="el-icon-right"></i>关闭右边</el-button>
            </li>
            <li v-show="visibleItem.other">
              <el-button link @click="closeTabs('other')"
                ><i class="el-icon-right"></i>关闭其他
              <el-button link @click="closeTabs('other')"><i class="el-icon-right"></i>关闭其他
              </el-button>
            </li>
          </ul>
@@ -141,56 +90,29 @@
          <loading v-show="$store.getters.isLoading()"></loading>
          <router-view v-slot="{ Component }">
            <keep-alive>
              <component
                :is="Component"
                :key="$route.name"
                v-if="
                  !$route.meta ||
                  ($route.meta && !$route.meta.hasOwnProperty('keepAlive'))
                "
              />
              <component :is="Component" :key="$route.name" v-if="
                !$route.meta ||
                ($route.meta && !$route.meta.hasOwnProperty('keepAlive'))
              " />
            </keep-alive>
            <component
              :is="Component"
              :key="$route.name"
              v-if="$route.meta && $route.meta.hasOwnProperty('keepAlive')"
            />
            <component :is="Component" :key="$route.name"
              v-if="$route.meta && $route.meta.hasOwnProperty('keepAlive')" />
          </router-view>
        </el-scrollbar>
      </div>
    </div>
    <el-drawer
      title="选择主题"
      v-model="drawer_model"
      direction="rtl"
      destroy-on-close
    >
    <el-drawer title="选择主题" v-model="drawer_model" direction="rtl" destroy-on-close>
      <div class="theme-selector">
        <div
          @click="changeTheme(item.name)"
          class="item"
          v-for="(item, index) in theme_color"
          :key="index"
          :style="{ background: item.color }"
        >
          <div
            v-show="item.leftColor"
            :style="{ background: item.leftColor }"
            style="height: 100%; width: 20px"
            class="t-left"
          ></div>
        <div @click="changeTheme(item.name)" class="item" v-for="(item, index) in theme_color" :key="index"
          :style="{ background: item.color }">
          <div v-show="item.leftColor" :style="{ background: item.leftColor }" style="height: 100%; width: 20px"
            class="t-left"></div>
          <div class="t-right"></div>
        </div>
      </div>
    </el-drawer>
    <el-drawer
      title="消息列表"
      v-model="messageModel"
      direction="rtl"
      destroy-on-close
      size="40%"
    >
    <el-drawer title="消息列表" v-model="messageModel" direction="rtl" destroy-on-close size="40%">
      <Message :list="messageList"></Message>
    </el-drawer>
  </div>
@@ -339,7 +261,7 @@
        setTimeout(createSocket, 10000);
      };
      client.onerror = function () {};
      client.onerror = function () { };
    };
    const changeTheme = (name) => {
@@ -606,7 +528,7 @@
      }
      createSocket("ws://127.0.0.1:9296/" + _userInfo.userName);
      // createSocket("ws://127.0.0.1:9296");
      Object.assign(_config.$tabs, { open: open, close: close });
      http.get("api/Sys_Menu/getTreeMenu", {}, true).then((data) => {
@@ -792,6 +714,7 @@
  font-size: 14px;
  color: #333;
  box-shadow: 2px 2px 3px 0 rgb(182 182 182 / 20%);
  i,
  button {
    font-size: 14px !important;
@@ -814,12 +737,8 @@
  letter-spacing: 1px;
}
.el-tabs.el-tabs--top.el-tabs--border-card.header-navigation
  > .el-tabs__header
  .el-tabs__item:last-child,
.el-tabs--top.el-tabs--border-card.header-navigation
  > .el-tabs__header
  .el-tabs__item:nth-child(2) {
.el-tabs.el-tabs--top.el-tabs--border-card.header-navigation>.el-tabs__header .el-tabs__item:last-child,
.el-tabs--top.el-tabs--border-card.header-navigation>.el-tabs__header .el-tabs__item:nth-child(2) {
  padding: 0;
}
Code/WMS/WIDESEA_WMSClient/src/views/stock/stockChat.vue
@@ -217,6 +217,10 @@
}
function hasCargo(location) {
    // 如果LocationStatus=100,即使没有库存信息也显示货位
    if (location.locationStatus === 100) {
        return true
    }
    return Number(location.stockQuantity || 0) > 0 || ((location.details && location.details.length > 0) || false)
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_Core/Extensions/WebSocketSetup.cs
@@ -11,22 +11,37 @@
{
    public static class WebSocketSetup
    {
        public static void AddWebSocketSetup(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
        //public static void AddWebSocketSetup(this IServiceCollection services)
        //{
        //    if (services == null) throw new ArgumentNullException(nameof(services));
            int port = AppSettings.Get("WebSocketPort").ObjToInt();
            if (port == 0)
            {
                port = 9296;
            }
            services.AddSingleton(x =>
            {
                WebSocketServer socketServer = new WebSocketServer();
                socketServer.ServerStart(port);
                return socketServer;
            });
        //    int port = AppSettings.Get("WebSocketPort").ObjToInt();
        //    if (port == 0)
        //    {
        //        port = 9296;
        //    }
        //    services.AddSingleton(x =>
        //    {
        //        WebSocketServer socketServer = new WebSocketServer();
        //        socketServer.ServerStart(port);
        //        return socketServer;
        //    });
        //}
        public static void AddWebSocketSetup(this IServiceCollection services)
        {
            if (services == null) throw new ArgumentNullException(nameof(services));
            int port = AppSettings.Get("WebSocketPort").ObjToInt();
            if (port == 0)
            {
                port = 9296;
            }
            // 直接创建并启动 WebSocket 服务器
            WebSocketServer socketServer = new WebSocketServer();
            socketServer.ServerStart(port);
            services.AddSingleton(socketServer);
        }
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService_AGV.cs
@@ -1,681 +1,678 @@
using Mapster;
using System.Text.Json;
using WIDESEA_Common.Constants;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using WIDESEA_Core;
using WIDESEA_Core.Enums;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.Task;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        public string AGV_OutTaskComplete = WIDESEA_Core.Helper.AppSettings.Configuration["AGV_OutTaskComplete"];
        public string WCS_ReceiveTask = WIDESEA_Core.Helper.AppSettings.Configuration["WCS_ReceiveTask"];
        /// <summary>
        /// 极卷库出入库申请
        /// </summary>
        public async Task<AGVResponse> ApplyInOutAsync(ApplyInOutDto applyInOutDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyInOutRequest(applyInOutDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var existingTask = await BaseDal.QueryFirstAsync(x => x.PalletCode == applyInOutDto.TrayNumber || x.OrderNo == applyInOutDto.TaskId);
                if (existingTask != null)
                    return response.Error($"WMS已有当前任务,不可重复下发,任务号:{applyInOutDto.TaskId}");
                var task = BuildAgvTask(applyInOutDto);
                if (applyInOutDto.InOut == 1)
                {
                    var inboundResult = await CreateAgvInboundTaskAsync(task, applyInOutDto);
                    if (inboundResult != null)
                        return inboundResult;
                }
                else
                {
                    var outboundResult = await CreateAgvOutboundTaskAsync(task, applyInOutDto);
                    if (outboundResult != null)
                        return outboundResult;
                }
                return response.OK(BuildAgvDataDto(task, applyInOutDto));
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务创建接口错误: {ex.Message}");
            }
        }
        /// <summary>
        /// 手动出库完成反馈给AGV
        /// </summary>
        public async Task<WebResponseContent> OutTaskComplete(OutTaskCompleteDto outTaskCompleteDto)
        {
            WebResponseContent response = new WebResponseContent();
            try
            {
                var validationMessage = ValidateOutTaskCompleteRequest(outTaskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == outTaskCompleteDto.TaskId);
                if (task == null)
                    return response.Error("未找到任务信息");
                outTaskCompleteDto.ReqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                var httpResponse = _httpClientHelper.Post<AGVResponse>(AGV_OutTaskComplete, outTaskCompleteDto.ToJson()).Data;
                if (httpResponse == null || httpResponse.Data == null)
                    return response.Error(httpResponse?.Msg ?? "AGV接口调用异常");
                if (!httpResponse.Code)
                        return response.Error(string.IsNullOrWhiteSpace(httpResponse.Msg) ? "AGV接口调用失败" : httpResponse.Msg);
                var syncResult = await CompleteLocalOutboundAfterAgvAckAsync(task);
                return syncResult.Status ? response.OK(httpResponse.Msg) : syncResult;
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// 输送线申请进入
        /// </summary>
        public async Task<AGVResponse> ApplyEnterAsync(ApplyEnterDto applyEnterDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyEnterRequest(applyEnterDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == applyEnterDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{applyEnterDto.TaskId}");
                if (CanApplyEnter(task, applyEnterDto))
                    return response.OK();
                return response.Error($"输送线{applyEnterDto.DevId}当前繁忙,请稍后重试");
            }
            catch (Exception ex)
            {
                return response.Error($"WMS输送线申请接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// 取放货完成
        /// </summary>
        public async Task<AGVResponse> TaskCompleteAsync(TaskCompleteDto taskCompleteDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCompleteRequest(taskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCompleteDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{taskCompleteDto.TaskId}");
                return taskCompleteDto.InOut == 2
                    ? await CompleteAgvOutboundTaskAsync(task)
                    : await CompleteAgvInboundTaskAsync(task);
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS取放货完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// 任务取消
        /// </summary>
        public async Task<AGVResponse> TaskCancelAsync(TaskCancelDto taskCancelDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCancelRequest(taskCancelDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCancelDto.TaskId);
                if (task == null)
                    return response.OK();
                if (task.TaskStatus == (int)TaskInStatusEnum.InNew)
                    return await CancelAgvInboundTask(task);
                if (task.TaskStatus == (int)TaskOutStatusEnum.OutNew)
                    return await CancelAgvOutboundTaskAsync(task);
                return response.Error("任务已经在执行中,不可取消");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务取消接口错误:{ex.Message}");
            }
        }
        #region 参数验证
        private static string? ValidateApplyInOutRequest(ApplyInOutDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TrayNumber)) return "托盘号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialType)) return "物料类型不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialName)) return "物料描述不能为空";
            if (string.IsNullOrWhiteSpace(dto.ReqTime)) return "请求时间不能为空";
            if (dto.Floor != 1 && dto.Floor != 2) return $"楼层段错误,必须为1(模切段)或2(卷绕段),当前值:{dto.Floor}";
            if (dto.YinYang != 1 && dto.YinYang != 2) return $"阴阳极错误,必须为1(阴极)或2(阳极),当前值:{dto.YinYang}";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            if (dto.InOut == 1 && (dto.Width == null || dto.Width <= 0)) return "入库时宽度不能为空且必须大于0";
            if (dto.InOut == 1 && string.IsNullOrWhiteSpace(dto.Group)) return "入库时整托组别不能为空";
            return null;
        }
        private static string? ValidateOutTaskCompleteRequest(OutTaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "出库口编号不能为空";
            return null;
        }
        private static string? ValidateApplyEnterRequest(ApplyEnterDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCompleteRequest(TaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCancelRequest(TaskCancelDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            return null;
        }
        #endregion 参数验证
        #region 具体实现
        // 出入库共用创建任务
        private Dt_Task BuildAgvTask(ApplyInOutDto dto)
        {
            var task = new Dt_Task
            {
                OrderNo = dto.TaskId,
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                Grade = 1,
                Creater = "AGV",
                CreateDate = DateTime.Now,
                Remark = $"物料类型:{dto.MaterialType},物料描述:{dto.MaterialName}"
            };
            if (dto.YinYang == 1)
            {
                task.Roadway = WarehouseEnum.FJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.FJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10010" : "D10020";
                task.NextAddress = "D10080";
                task.TargetAddress = "阴极卷库";
            }
            else
            {
                task.Roadway = WarehouseEnum.ZJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.ZJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10100" : "D10090";
                task.NextAddress = "D10160";
                task.TargetAddress = "正极卷库";
            }
            return task;
        }
        // 构建返回AGV出入库请求体
        private AGVDataDto BuildAgvDataDto(Dt_Task task, ApplyInOutDto dto)
        {
            return new AGVDataDto
            {
                DevId = dto.InOut == 1 ? task.SourceAddress : task.TargetAddress,
                TrayNumber = task.PalletCode,
                Group = dto.Group,
                Width = dto.Width ?? 0,
                LabelNumber = dto.LabelNumber,
                ProductNo = dto.ProductNo,
                ProductName = dto.ProductName,
                Quantity = dto.Quantity,
                UomCode = dto.UomCode,
                ProductType = dto.ProductType,
                Equipment = dto.Equipment,
                ProductionDate = dto.ProductionDate,
                LowerLimitTime = dto.LowerLimitTime,
                WarningTime = dto.WarningTime,
                OverdueTime = dto.OverdueTime
            };
        }
        // 入库创建
        private async Task<AGVResponse?> CreateAgvInboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo != null)
                return response.Error($"当前托盘{dto.TrayNumber}已经入库了");
            // 创建库存明细
            var details = new Dt_StockInfoDetail
            {
                MaterielCode = dto.ProductNo,
                MaterielName = dto.ProductName,
                StockQuantity = int.TryParse(dto.Quantity, out int quantity) ? quantity : 0,
                Unit = dto.UomCode,
                OrderNo = dto.ProductNo,
                ProductionDate =dto.ProductionDate,
                EffectiveDate = dto.LowerLimitTime,
                SerialNumber = dto.TrayNumber,
                Status = (int)StockStatusEmun.入库确认,
                InboundOrderRowNo = 1,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}"
            };
            // 创建库存主记录
            var stock = new Dt_StockInfo
            {
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                WarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1,
                StockStatus = (int)StockStatusEmun.入库确认,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}",
                Details = new List<Dt_StockInfoDetail> { details }
            };
            task.TaskType = (int)TaskInboundTypeEnum.Inbound;
            task.TaskStatus = (int)TaskInStatusEnum.InNew;
            task.CurrentAddress = task.SourceAddress;
            _unitOfWorkManage.BeginTran();
            try
            {
                // 先创建任务
                var taskResult = await BaseDal.AddDataAsync(task) > 0;
                if (!taskResult)
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("入库任务创建失败");
using Mapster;
using System.Text.Json;
using WIDESEA_Common.Constants;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Common.StockEnum;
using WIDESEA_Common.TaskEnum;
using WIDESEA_Common.WareHouseEnum;
using WIDESEA_Core;
using WIDESEA_Core.Enums;
using WIDESEA_Core.Helper;
using WIDESEA_DTO.Task;
using WIDESEA_IStockService;
using WIDESEA_Model.Models;
namespace WIDESEA_TaskInfoService
{
    public partial class TaskService
    {
        public string AGV_OutTaskComplete = WIDESEA_Core.Helper.AppSettings.Configuration["AGV_OutTaskComplete"];
        public string WCS_ReceiveTask = WIDESEA_Core.Helper.AppSettings.Configuration["WCS_ReceiveTask"];
        /// <summary>
        /// 极卷库出入库申请
        /// </summary>
        public async Task<AGVResponse> ApplyInOutAsync(ApplyInOutDto applyInOutDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyInOutRequest(applyInOutDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var existingTask = await BaseDal.QueryFirstAsync(x => x.PalletCode == applyInOutDto.TrayNumber || x.OrderNo == applyInOutDto.TaskId);
                if (existingTask != null)
                    return response.Error($"WMS已有当前任务,不可重复下发,任务号:{applyInOutDto.TaskId}");
                var task = BuildAgvTask(applyInOutDto);
                if (applyInOutDto.InOut == 1)
                {
                    var inboundResult = await CreateAgvInboundTaskAsync(task, applyInOutDto);
                    if (inboundResult != null)
                        return inboundResult;
                }
                else
                {
                    var outboundResult = await CreateAgvOutboundTaskAsync(task, applyInOutDto);
                    if (outboundResult != null)
                        return outboundResult;
                }
                return response.OK(BuildAgvDataDto(task, applyInOutDto));
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务创建接口错误: {ex.Message}");
            }
        }
        /// <summary>
        /// 手动出库完成反馈给AGV
        /// </summary>
        public async Task<WebResponseContent> OutTaskComplete(OutTaskCompleteDto outTaskCompleteDto)
        {
            WebResponseContent response = new WebResponseContent();
            try
            {
                var validationMessage = ValidateOutTaskCompleteRequest(outTaskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == outTaskCompleteDto.TaskId);
                if (task == null)
                    return response.Error("未找到任务信息");
                outTaskCompleteDto.ReqTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
                var httpResponse = _httpClientHelper.Post<AGVResponse>(AGV_OutTaskComplete, outTaskCompleteDto.ToJson()).Data;
                if (httpResponse == null || httpResponse.Data == null)
                    return response.Error(httpResponse?.Msg ?? "AGV接口调用异常");
                if (!httpResponse.Code)
                        return response.Error(string.IsNullOrWhiteSpace(httpResponse.Msg) ? "AGV接口调用失败" : httpResponse.Msg);
                var syncResult = await CompleteLocalOutboundAfterAgvAckAsync(task);
                return syncResult.Status ? response.OK(httpResponse.Msg) : syncResult;
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// 输送线申请进入
        /// </summary>
        public async Task<AGVResponse> ApplyEnterAsync(ApplyEnterDto applyEnterDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateApplyEnterRequest(applyEnterDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == applyEnterDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{applyEnterDto.TaskId}");
                if (CanApplyEnter(task, applyEnterDto))
                    return response.OK();
                return response.Error($"输送线{applyEnterDto.DevId}当前繁忙,请稍后重试");
            }
            catch (Exception ex)
            {
                return response.Error($"WMS输送线申请接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// 取放货完成
        /// </summary>
        public async Task<AGVResponse> TaskCompleteAsync(TaskCompleteDto taskCompleteDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCompleteRequest(taskCompleteDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCompleteDto.TaskId);
                if (task == null)
                    return response.Error($"未找到任务信息,任务号:{taskCompleteDto.TaskId}");
                return taskCompleteDto.InOut == 2
                    ? await CompleteAgvOutboundTaskAsync(task)
                    : await CompleteAgvInboundTaskAsync(task);
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS取放货完成接口错误:{ex.Message}");
            }
        }
        /// <summary>
        /// 任务取消
        /// </summary>
        public async Task<AGVResponse> TaskCancelAsync(TaskCancelDto taskCancelDto)
        {
            AGVResponse response = new AGVResponse();
            try
            {
                var validationMessage = ValidateTaskCancelRequest(taskCancelDto);
                if (validationMessage != null)
                    return response.Error(validationMessage);
                var task = await BaseDal.QueryFirstAsync(x => x.OrderNo == taskCancelDto.TaskId);
                if (task == null)
                    return response.OK();
                if (task.TaskStatus == (int)TaskInStatusEnum.InNew)
                    return await CancelAgvInboundTask(task);
                if (task.TaskStatus == (int)TaskOutStatusEnum.OutNew)
                    return await CancelAgvOutboundTaskAsync(task);
                return response.Error("任务已经在执行中,不可取消");
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"WMS任务取消接口错误:{ex.Message}");
            }
        }
        #region 参数验证
        private static string? ValidateApplyInOutRequest(ApplyInOutDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TrayNumber)) return "托盘号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialType)) return "物料类型不能为空";
            if (string.IsNullOrWhiteSpace(dto.MaterialName)) return "物料描述不能为空";
            if (string.IsNullOrWhiteSpace(dto.ReqTime)) return "请求时间不能为空";
            if (dto.Floor != 1 && dto.Floor != 2) return $"楼层段错误,必须为1(模切段)或2(卷绕段),当前值:{dto.Floor}";
            if (dto.YinYang != 1 && dto.YinYang != 2) return $"阴阳极错误,必须为1(阴极)或2(阳极),当前值:{dto.YinYang}";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            if (dto.InOut == 1 && (dto.Width == null || dto.Width <= 0)) return "入库时宽度不能为空且必须大于0";
            if (dto.InOut == 1 && string.IsNullOrWhiteSpace(dto.Group)) return "入库时整托组别不能为空";
            return null;
        }
        private static string? ValidateOutTaskCompleteRequest(OutTaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "出库口编号不能为空";
            return null;
        }
        private static string? ValidateApplyEnterRequest(ApplyEnterDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCompleteRequest(TaskCompleteDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            if (string.IsNullOrWhiteSpace(dto.DevId)) return "设备编号不能为空";
            if (dto.InOut != 1 && dto.InOut != 2) return $"出入库类型错误,必须为1(入库)或2(出库),当前值:{dto.InOut}";
            return null;
        }
        private static string? ValidateTaskCancelRequest(TaskCancelDto dto)
        {
            if (dto == null) return "请求参数不能为空";
            if (string.IsNullOrWhiteSpace(dto.TaskId)) return "任务号不能为空";
            return null;
        }
        #endregion 参数验证
        #region 具体实现
        // 出入库共用创建任务
        private Dt_Task BuildAgvTask(ApplyInOutDto dto)
        {
            var task = new Dt_Task
            {
                OrderNo = dto.TaskId,
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                Grade = 1,
                Creater = "AGV",
                CreateDate = DateTime.Now,
                Remark = $"物料类型:{dto.MaterialType},物料描述:{dto.MaterialName}"
            };
            if (dto.YinYang == 1)
            {
                task.Roadway = WarehouseEnum.FJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.FJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10010" : "D10020";
                task.NextAddress = "D10080";
                task.TargetAddress = "阴极卷库";
            }
            else
            {
                task.Roadway = WarehouseEnum.ZJ1.ToString();
                task.WarehouseId = (int)WarehouseEnum.ZJ1;
                task.SourceAddress = dto.InOut == 1 ? "D10100" : "D10090";
                task.NextAddress = "D10160";
                task.TargetAddress = "正极卷库";
            }
            return task;
        }
        // 构建返回AGV出入库请求体
        private AGVDataDto BuildAgvDataDto(Dt_Task task, ApplyInOutDto dto)
        {
            return new AGVDataDto
            {
                DevId = dto.InOut == 1 ? task.SourceAddress : task.TargetAddress,
                TrayNumber = task.PalletCode,
                Group = dto.Group,
                Width = dto.Width ?? 0,
                LabelNumber = dto.LabelNumber,
                ProductNo = dto.ProductNo,
                ProductName = dto.ProductName,
                Quantity = dto.Quantity,
                UomCode = dto.UomCode,
                ProductType = dto.ProductType,
                Equipment = dto.Equipment,
                ProductionDate = dto.ProductionDate,
                LowerLimitTime = dto.LowerLimitTime,
                WarningTime = dto.WarningTime,
                OverdueTime = dto.OverdueTime
            };
        }
        // 入库创建
        private async Task<AGVResponse?> CreateAgvInboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo != null)
                return response.Error($"当前托盘{dto.TrayNumber}已经入库了");
            // 创建库存明细
            var details = new Dt_StockInfoDetail
            {
                MaterielCode = dto.ProductNo,
                MaterielName = dto.ProductName,
                StockQuantity = int.TryParse(dto.Quantity, out int quantity) ? quantity : 0,
                Unit = dto.UomCode,
                OrderNo = dto.ProductNo,
                ProductionDate =dto.ProductionDate,
                EffectiveDate = dto.LowerLimitTime,
                SerialNumber = dto.TrayNumber,
                Status = (int)StockStatusEmun.入库确认,
                InboundOrderRowNo = 1,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}"
            };
            // 创建库存主记录
            var stock = new Dt_StockInfo
            {
                PalletCode = dto.TrayNumber,
                PalletType = dto.Floor,
                WarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1,
                StockStatus = (int)StockStatusEmun.入库确认,
                Creater = StockConstants.AGV_USER,
                CreateDate = DateTime.Now,
                Remark = $"AGV入库任务创建,任务号:{dto.TaskId}",
                Details = new List<Dt_StockInfoDetail> { details }
            };
            task.TaskType = (int)TaskInboundTypeEnum.Inbound;
            task.TaskStatus = (int)TaskInStatusEnum.InNew;
            task.CurrentAddress = task.SourceAddress;
            _unitOfWorkManage.BeginTran();
            try
            {
                // 先创建任务
                var taskResult = await BaseDal.AddDataAsync(task) > 0;
                if (!taskResult)
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("入库任务创建失败");
                }
                var result = _stockInfoService.Repository.AddData(stock, x => x.Details);
                if (result)
                {
                    _unitOfWorkManage.CommitTran();
                    return null;
                {
                    _unitOfWorkManage.CommitTran();
                    return null;
                }
                else
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("库存信息创建失败");
                else
                {
                    _unitOfWorkManage.RollbackTran();
                    return response.Error("库存信息创建失败");
                }
                // 使用库存服务添加库存主记录和明细
                //var stockResult = await _stockInfoService.AddStockWithDetailsUsingTransactionAsync(stock);
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"入库任务创建异常:{ex.Message}");
            }
        }
        // 出库创建
        private async Task<AGVResponse?> CreateAgvOutboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            // 检查库存是否存在
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo == null)
                return response.Error($"未找到托盘{dto.TrayNumber}的库存信息");
            // 检查库存是否有明细(即是否真的有库存)
            if (stockInfo.Details == null || !stockInfo.Details.Any())
                return response.Error($"托盘{dto.TrayNumber}没有库存明细,无法出库");
            // 检查库存总数量是否大于0
            var totalQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
            if (totalQuantity <= 0)
                return response.Error($"托盘{dto.TrayNumber}库存数量不足,无法出库");
            // 根据dto参数进一步验证库存信息
            if (!string.IsNullOrEmpty(dto.ProductNo))
            {
                // 检查库存明细中是否包含指定的物料编码
                var hasMatchingMaterial = stockInfo.Details.Any(d => d.MaterielCode == dto.ProductNo);
                if (!hasMatchingMaterial)
                    return response.Error($"托盘{dto.TrayNumber}中没有物料编码为{dto.ProductNo}的库存,无法出库");
            }
            // 检查库存状态是否允许出库
            if (stockInfo.StockStatus != (int)StockStatusEmun.入库完成)
                return response.Error($"托盘{dto.TrayNumber}正在移动中,请稍后!");
            // 检查货位信息
            var locationInfo = await _locationInfoService.GetLocationInfo(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            // 检查货位状态是否允许出库
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            // 验证仓库ID是否匹配(根据dto的阴阳极参数)
            var expectedWarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1;
            if (stockInfo.WarehouseId != expectedWarehouseId)
                return response.Error($"托盘{dto.TrayNumber}不在预期的仓库中,无法出库");
            task.TaskType = (int)TaskOutboundTypeEnum.Outbound;
            task.TaskStatus = (int)TaskOutStatusEnum.OutNew;
            task.SourceAddress = stockInfo.LocationCode;
            task.CurrentAddress = stockInfo.LocationCode;
            task.TargetAddress = dto.YinYang == 1 ? "D10020" : "D10090";
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            stockInfo.StockStatus = (int)StockStatusEmun.出库锁定;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStockLock;
            _unitOfWorkManage.BeginTran();
            var addTaskResult = await BaseDal.AddDataAsync(task) > 0;
            var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
            var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
            if (!addTaskResult || !updateLocationResult || !updateStockResult)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务创建失败");
            }
            _unitOfWorkManage.CommitTran();
            return null;
        }
        private async Task<WebResponseContent> CompleteLocalOutboundAfterAgvAckAsync(Dt_Task task)
        {
            task.TaskStatus = (int)TaskOutStatusEnum.Line_OutFinish;
            _unitOfWorkManage.BeginTran();
            var updateResult = BaseDal.UpdateData(task);
            if (!updateResult)
            {
                _unitOfWorkManage.RollbackTran();
                return WebResponseContent.Instance.Error("AGV完成回传后,任务更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return WebResponseContent.Instance.OK();
        }
        private bool CanApplyEnter(Dt_Task task, ApplyEnterDto dto)
        {
            if (dto.InOut == 1)
            {
                var hasExecutingOutTask = BaseDal.QueryFirst(x => x.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && x.TargetAddress == task.SourceAddress
                    && (x.TaskStatus == (int)TaskOutStatusEnum.SC_OutExecuting
                        || x.TaskStatus == (int)TaskOutStatusEnum.Line_OutExecuting));
                // 如果没有正在执行的出库任务,则允许入库
                return hasExecutingOutTask == null;
            }
            else
            {
                return task.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && task.TaskStatus == (int)TaskStatusEnum.Line_Finish;
            }
        }
        // WCS入库完成
        private async Task<WebResponseContent> CompleteAgvInboundTaskAsync(CreateTaskDto taskDto)
        {
            WebResponseContent response = new WebResponseContent();
            var task = await BaseDal.QueryFirstAsync(x => x.PalletType == taskDto.PalletType);
            if (task == null)
                return response.Error($"没有当前托盘{taskDto.PalletType}入库任务");
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到货位{task.TargetAddress}的信息");
            if (locationInfo.LocationStatus == (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态不是空闲状态,无法入库");
            // 更新货位状态为占用
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskInStatusEnum.InFinish;
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            _unitOfWorkManage.BeginTran();
            var addStockResult = _stockInfoService.UpdateData(stockInfo);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!addStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,添加库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV出库完成
        private async Task<AGVResponse> CompleteAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            locationInfo.LocationStatus = (int)LocationStatusEnum.Free;
            task.TaskStatus = (int)TaskOutStatusEnum.OutFinish;
            _unitOfWorkManage.BeginTran();
            //var deleteStockResult = _stockInfoService.DeleteData(stockInfo)
            var deleteStockResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!deleteStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库完成后,本地库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV已放货,准备输送线入库
        private async Task<AGVResponse> CompleteAgvInboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var availableLocation = await _locationInfoService.GetLocationInfo(task.Roadway);
            if (availableLocation == null)
                return response.Error("无可用的入库货位");
            task.TargetAddress = availableLocation.LocationCode;
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            task.TaskStatus = (int)TaskInStatusEnum.Line_InExecuting;
            task.Dispatchertime = DateTime.Now;
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到托盘{task.TargetAddress}的货位信息");
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.Free)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            var existingStock = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (existingStock != null)
                return response.Error($"托盘{task.PalletCode}的库存信息已存在,请勿重复入库");
            //Dt_StockInfo stockInfo = new Dt_StockInfo
            //{
            //    PalletCode = task.PalletCode,
            //    StockStatus = (int)StockStatusEmun.入库确认,
            //    LocationCode = locationInfo.LocationCode,
            //    WarehouseId = task.WarehouseId,
            //    Creater = "AGV",
            //    CreateDate = DateTime.Now
            //};
            locationInfo.LocationStatus = (int)LocationStatusEnum.FreeLock;
            _unitOfWorkManage.BeginTran();
            var updateTaskResult = BaseDal.UpdateData(task);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            //var addStockResult = _stockInfoService.AddData(stockInfo);
            if (!updateTaskResult || !updateLocationResult.Status /*|| !addStockResult.Status*/)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,本地任务、库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV入库取消
        private async Task<AGVResponse> CancelAgvInboundTask(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            task.TaskStatus = (int)TaskInStatusEnum.InCancel;
            _unitOfWorkManage.BeginTran();
            try
            {
                var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
                if (stockInfo != null)
                {
                    var deleteResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
                    if (!deleteResult.Status)
                    {
                        _unitOfWorkManage.RollbackTran();
                        return response.Error($"删除库存失败: {deleteResult.Message}");
                    }
                }
                BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                _unitOfWorkManage.CommitTran();
                return response.OK();
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"取消入库任务时发生异常: {ex.Message}");
            }
        }
        // AGV出库取消
        private async Task<AGVResponse> CancelAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskOutStatusEnum.OutCancel;
            _unitOfWorkManage.BeginTran();
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            var updateStockResult = _stockInfoService.UpdateData(stockInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!updateLocationResult.Status || !updateStockResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务取消失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        #endregion 具体实现
    }
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"入库任务创建异常:{ex.Message}");
            }
        }
        // 出库创建
        private async Task<AGVResponse?> CreateAgvOutboundTaskAsync(Dt_Task task, ApplyInOutDto dto)
        {
            AGVResponse response = new AGVResponse();
            // 检查库存是否存在
            var stockInfo = await _stockInfoService.GetStockInfoAsync(dto.TrayNumber);
            if (stockInfo == null)
                return response.Error($"未找到托盘{dto.TrayNumber}的库存信息");
            // 检查库存是否有明细(即是否真的有库存)
            if (stockInfo.Details == null || !stockInfo.Details.Any())
                return response.Error($"托盘{dto.TrayNumber}没有库存明细,无法出库");
            // 检查库存总数量是否大于0
            var totalQuantity = stockInfo.Details.Sum(d => d.StockQuantity);
            if (totalQuantity <= 0)
                return response.Error($"托盘{dto.TrayNumber}库存数量不足,无法出库");
            // 根据dto参数进一步验证库存信息
            if (!string.IsNullOrEmpty(dto.ProductNo))
            {
                // 检查库存明细中是否包含指定的物料编码
                var hasMatchingMaterial = stockInfo.Details.Any(d => d.MaterielCode == dto.ProductNo);
                if (!hasMatchingMaterial)
                    return response.Error($"托盘{dto.TrayNumber}中没有物料编码为{dto.ProductNo}的库存,无法出库");
            }
            // 检查库存状态是否允许出库
            if (stockInfo.StockStatus != (int)StockStatusEmun.入库完成)
                return response.Error($"托盘{dto.TrayNumber}正在移动中,请稍后!");
            // 检查货位信息
            var locationInfo = await _locationInfoService.GetLocationInfo(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            // 检查货位状态是否允许出库
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            // 验证仓库ID是否匹配(根据dto的阴阳极参数)
            var expectedWarehouseId = dto.YinYang == 1 ? (int)WarehouseEnum.FJ1 : (int)WarehouseEnum.ZJ1;
            if (stockInfo.WarehouseId != expectedWarehouseId)
                return response.Error($"托盘{dto.TrayNumber}不在预期的仓库中,无法出库");
            task.TaskType = (int)TaskOutboundTypeEnum.Outbound;
            task.TaskStatus = (int)TaskOutStatusEnum.OutNew;
            task.SourceAddress = stockInfo.LocationCode;
            task.CurrentAddress = stockInfo.LocationCode;
            task.TargetAddress = dto.YinYang == 1 ? "D10020" : "D10090";
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            stockInfo.StockStatus = (int)StockStatusEmun.出库锁定;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStockLock;
            _unitOfWorkManage.BeginTran();
            var addTaskResult = await BaseDal.AddDataAsync(task) > 0;
            var updateLocationResult = await _locationInfoService.UpdateLocationInfoAsync(locationInfo);
            var updateStockResult = await _stockInfoService.UpdateStockAsync(stockInfo);
            if (!addTaskResult || !updateLocationResult || !updateStockResult)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务创建失败");
            }
            _unitOfWorkManage.CommitTran();
            return null;
        }
        private async Task<WebResponseContent> CompleteLocalOutboundAfterAgvAckAsync(Dt_Task task)
        {
            task.TaskStatus = (int)TaskOutStatusEnum.Line_OutFinish;
            _unitOfWorkManage.BeginTran();
            var updateResult = BaseDal.UpdateData(task);
            if (!updateResult)
            {
                _unitOfWorkManage.RollbackTran();
                return WebResponseContent.Instance.Error("AGV完成回传后,任务更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return WebResponseContent.Instance.OK();
        }
        private bool CanApplyEnter(Dt_Task task, ApplyEnterDto dto)
        {
            if (dto.InOut == 1)
            {
                var hasExecutingOutTask = BaseDal.QueryFirst(x => x.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && x.TargetAddress == task.SourceAddress
                    && (x.TaskStatus == (int)TaskOutStatusEnum.SC_OutExecuting
                        || x.TaskStatus == (int)TaskOutStatusEnum.Line_OutExecuting));
                // 如果没有正在执行的出库任务,则允许入库
                return hasExecutingOutTask == null;
            }
            else
            {
                return task.TaskType == (int)TaskOutboundTypeEnum.Outbound
                    && task.TaskStatus == (int)TaskStatusEnum.Line_Finish;
            }
        }
        // WCS入库完成
        private async Task<WebResponseContent> CompleteAgvInboundTaskAsync(CreateTaskDto taskDto)
        {
            WebResponseContent response = new WebResponseContent();
            var task = await BaseDal.QueryFirstAsync(x => x.PalletType == taskDto.PalletType);
            if (task == null)
                return response.Error($"没有当前托盘{taskDto.PalletType}入库任务");
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到货位{task.TargetAddress}的信息");
            if (locationInfo.LocationStatus == (int)LocationStatusEnum.InStock)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态不是空闲状态,无法入库");
            // 更新货位状态为占用
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskInStatusEnum.InFinish;
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            _unitOfWorkManage.BeginTran();
            var addStockResult = _stockInfoService.UpdateData(stockInfo);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!addStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,添加库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV出库完成
        private async Task<AGVResponse> CompleteAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            locationInfo.LocationStatus = (int)LocationStatusEnum.Free;
            task.TaskStatus = (int)TaskOutStatusEnum.OutFinish;
            _unitOfWorkManage.BeginTran();
            //var deleteStockResult = _stockInfoService.DeleteData(stockInfo)
            var deleteStockResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!deleteStockResult.Status || !updateLocationResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库完成后,本地库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV已放货,准备输送线入库
        private async Task<AGVResponse> CompleteAgvInboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var availableLocation = await _locationInfoService.GetLocationInfo(task.Roadway);
            if (availableLocation == null)
                return response.Error("无可用的入库货位");
            task.TargetAddress = availableLocation.LocationCode;
            var wmsTaskDto = _mapper.Map<WMSTaskDTO>(task);
            var taskList = new List<WMSTaskDTO> { wmsTaskDto };
            var requestBody = JsonSerializer.Serialize(taskList);
            var httpResponse = _httpClientHelper.Post<WebResponseContent>(WCS_ReceiveTask, requestBody);
            if (httpResponse == null || httpResponse.Data == null || !httpResponse.Data.Status)
                return response.Error(httpResponse?.Data?.Message ?? "下发WCS失败");
            task.TaskStatus = (int)TaskInStatusEnum.Line_InExecuting;
            task.Dispatchertime = DateTime.Now;
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(task.TargetAddress);
            if (locationInfo == null)
                return response.Error($"未找到托盘{task.TargetAddress}的货位信息");
            if (locationInfo.LocationStatus != (int)LocationStatusEnum.Free)
                return response.Error($"当前货位{locationInfo.LocationStatus}状态信息错误");
            var existingStock = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (existingStock != null)
                return response.Error($"托盘{task.PalletCode}的库存信息已存在,请勿重复入库");
            //Dt_StockInfo stockInfo = new Dt_StockInfo
            //{
            //    PalletCode = task.PalletCode,
            //    StockStatus = (int)StockStatusEmun.入库确认,
            //    LocationCode = locationInfo.LocationCode,
            //    WarehouseId = task.WarehouseId,
            //    Creater = "AGV",
            //    CreateDate = DateTime.Now
            //};
            locationInfo.LocationStatus = (int)LocationStatusEnum.FreeLock;
            _unitOfWorkManage.BeginTran();
            var updateTaskResult = BaseDal.UpdateData(task);
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            //var addStockResult = _stockInfoService.AddData(stockInfo);
            if (!updateTaskResult || !updateLocationResult.Status /*|| !addStockResult.Status*/)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("入库完成后,本地任务、库存或货位更新失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        // AGV入库取消
        private async Task<AGVResponse> CancelAgvInboundTask(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            task.TaskStatus = (int)TaskInStatusEnum.InCancel;
            _unitOfWorkManage.BeginTran();
            try
            {
                var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
                if (stockInfo != null)
                {
                    var deleteResult = await _stockInfoService.DeleteStockWithDetailsAsync(stockInfo.Id);
                    if (!deleteResult.Status)
                    {
                        _unitOfWorkManage.RollbackTran();
                        return response.Error($"删除库存失败: {deleteResult.Message}");
                    }
                }
                BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
                _unitOfWorkManage.CommitTran();
                return response.OK();
            }
            catch (Exception ex)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error($"取消入库任务时发生异常: {ex.Message}");
            }
        }
        // AGV出库取消
        private async Task<AGVResponse> CancelAgvOutboundTaskAsync(Dt_Task task)
        {
            AGVResponse response = new AGVResponse();
            var stockInfo = await _stockInfoService.GetStockInfoAsync(task.PalletCode);
            if (stockInfo == null)
                return response.Error($"未找到托盘{task.PalletCode}的库存信息");
            var locationInfo = await _locationInfoService.GetLocationInfoAsync(stockInfo.LocationCode);
            if (locationInfo == null)
                return response.Error($"未找到托盘{stockInfo.LocationCode}的货位信息");
            if (stockInfo.StockStatus != (int)StockStatusEmun.出库锁定 || locationInfo.LocationStatus != (int)LocationStatusEnum.InStockLock)
                return response.Error($"当前库存{stockInfo.StockStatus}或者货位{locationInfo.LocationStatus}状态信息错误");
            stockInfo.StockStatus = (int)StockStatusEmun.入库完成;
            locationInfo.LocationStatus = (int)LocationStatusEnum.InStock;
            task.TaskStatus = (int)TaskOutStatusEnum.OutCancel;
            _unitOfWorkManage.BeginTran();
            var updateLocationResult = _locationInfoService.UpdateData(locationInfo);
            var updateStockResult = _stockInfoService.UpdateData(stockInfo);
            BaseDal.DeleteAndMoveIntoHty(task, App.User.UserId == 0 ? OperateTypeEnum.自动完成 : OperateTypeEnum.人工完成);
            if (!updateLocationResult.Status || !updateStockResult.Status)
            {
                _unitOfWorkManage.RollbackTran();
                return response.Error("出库任务取消失败");
            }
            _unitOfWorkManage.CommitTran();
            return response.OK();
        }
        #endregion 具体实现
    }
}
Code/WMS/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/Dashboard/DashboardController.cs
@@ -1,272 +1,373 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// 仪表盘
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// 总览数据
        /// </summary>
        [HttpGet("Overview"), AllowAnonymous]
        public async Task<WebResponseContent> Overview()
        {
            try
            {
                var today = DateTime.Today;
                var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
                // 今日入库数
                var todayInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 今日出库数
                var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 本月入库数
                var monthInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 本月出库数
                var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 当前总库存
                var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
                return WebResponseContent.Instance.OK(null, new
                {
                    TodayInbound = todayInbound,
                    TodayOutbound = todayOutbound,
                    MonthInbound = monthInbound,
                    MonthOutbound = monthOutbound,
                    TotalStock = totalStock
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"总览数据获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每日统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按日期分组。
        /// SqlSugar 的 GroupBy 不支持对 .Date 这样的计算列直接生成 SQL GROUP BY,
        /// 因此采用此方式以确保跨数据库兼容性。
        /// </remarks>
        [HttpGet("DailyStats"), AllowAnonymous]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            try
            {
                if (days <= 0) days = 30;
                if (days > 365) days = 365;
                var startDate = DateTime.Today.AddDays(-days + 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => t.InsertTime.Date)
                    .Select(g => new
                    {
                        Date = g.Key.ToString("yyyy-MM-dd"),
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Date)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每周统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按 ISO 8601 周键分组。
        /// 周键为 "YYYY-Www" 格式,无法直接在 SQL 层用 GROUP BY 实现。
        /// </remarks>
        [HttpGet("WeeklyStats"), AllowAnonymous]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            try
            {
                if (weeks <= 0) weeks = 12;
                var startDate = DateTime.Today.AddDays(-weeks * 7);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => GetWeekKey(t.InsertTime))
                    .Select(g => new
                    {
                        Week = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Week)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每周统计获取失败: {ex.Message}");
            }
        }
        private string GetWeekKey(DateTime date)
        {
            // 获取周一开始的周 (ISO 8601)
            var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
            var monday = date.AddDays(-diff);
            var weekNum = System.Globalization.CultureInfo.InvariantCulture
                .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            return $"{monday.Year}-W{weekNum:D2}";
        }
        /// <summary>
        /// 每月统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按年月分组。
        /// SqlSugar 的 GroupBy 不支持匿名对象 (Year, Month) 直接映射到 SQL GROUP BY,
        /// 因此采用此方式以确保跨数据库兼容性。
        /// </remarks>
        [HttpGet("MonthlyStats"), AllowAnonymous]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            try
            {
                if (months <= 0) months = 12;
                var startDate = DateTime.Today.AddMonths(-months + 1);
                startDate = new DateTime(startDate.Year, startDate.Month, 1);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
                    .Select(g => new
                    {
                        Month = $"{g.Key.Year}-{g.Key.Month:D2}",
                        Inbound = g.Count(t => t.TaskType >= 500 && t.TaskType < 600),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Month)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 库存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution"), AllowAnonymous]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            try
            {
                var today = DateTime.Today;
                // 使用 SQL 直接分组统计,避免加载所有数据到内存
                var result = new[]
                {
                    new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 7).CountAsync() },
                    new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 7 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 30).CountAsync() },
                    new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 30 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 90).CountAsync() },
                    new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 90).CountAsync() }
                };
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"库存库龄分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 各仓库库存分布
        /// </summary>
        /// <remarks>
        /// 使用 SQL GROUP BY 在数据库层面聚合,避免加载全部库存记录到内存。
        /// </remarks>
        [HttpGet("StockByWarehouse"), AllowAnonymous]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            try
            {
                // 查询仓库名称
                var warehouses = await _db.Queryable<Dt_Warehouse>()
                    .Select(w => new { w.WarehouseId, w.WarehouseName })
                    .ToListAsync();
                var warehouseDict = warehouses.ToDictionary(w => w.WarehouseId, w => w.WarehouseName);
                // 使用 SQL GROUP BY 在数据库层面聚合,仅返回聚合结果
                var stockGroups = await _db.Queryable<Dt_StockInfo>()
                    .GroupBy(s => s.WarehouseId)
                    .Select(s => new { s.WarehouseId, Count = SqlFunc.AggregateCount(s.Id) })
                    .ToListAsync();
                var result = stockGroups
                    .Select(g => new
                    {
                        Warehouse = warehouseDict.TryGetValue(g.WarehouseId, out var name) ? name : $"仓库{g.WarehouseId}",
                        Count = g.Count
                    })
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
    }
}
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using WIDESEA_Common.LocationEnum;
using WIDESEA_Core;
using WIDESEA_Model.Models;
namespace WIDESEA_WMSServer.Controllers.Dashboard
{
    /// <summary>
    /// 仪表盘
    /// </summary>
    [Route("api/Dashboard")]
    [ApiController]
    public class DashboardController : ControllerBase
    {
        private readonly ISqlSugarClient _db;
        public DashboardController(ISqlSugarClient db)
        {
            _db = db;
        }
        /// <summary>
        /// 总览数据
        /// </summary>
        [HttpGet("Overview"), AllowAnonymous]
        public async Task<WebResponseContent> Overview()
        {
            try
            {
                var today = DateTime.Today;
                var firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
                // 今日入库数
                var todayInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 今日出库数
                var todayOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= today && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 本月入库数
                var monthInbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 500 && t.TaskType < 600)
                    .CountAsync();
                // 本月出库数
                var monthOutbound = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= firstDayOfMonth && t.TaskType >= 100 && t.TaskType < 200)
                    .CountAsync();
                // 当前总库存
                var totalStock = await _db.Queryable<Dt_StockInfo>().CountAsync();
                return WebResponseContent.Instance.OK(null, new
                {
                    TodayInbound = todayInbound,
                    TodayOutbound = todayOutbound,
                    MonthInbound = monthInbound,
                    MonthOutbound = monthOutbound,
                    TotalStock = totalStock
                });
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"总览数据获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每日统计
        /// </summary>
        [HttpGet("DailyStats"), AllowAnonymous]
        public async Task<WebResponseContent> DailyStats([FromQuery] int days = 30)
        {
            try
            {
                if (days <= 0) days = 30;
                if (days > 365) days = 365;
                var startDate = DateTime.Today.AddDays(-days + 1);
                var endDate = DateTime.Today; // 包含今天
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate && t.InsertTime <= endDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                // 生成日期范围
                var allDates = new List<DateTime>();
                for (var date = startDate; date <= endDate; date = date.AddDays(1))
                {
                    allDates.Add(date);
                }
                // 按日期分组统计
                var groupedData = query
                    .GroupBy(t => t.InsertTime.Date)
                    .Select(g => new
                    {
                        Date = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .ToDictionary(x => x.Date, x => x);
                // 补全缺失日期
                var result = allDates.Select(date =>
                {
                    if (groupedData.TryGetValue(date, out var data))
                    {
                        return new
                        {
                            Date = date.ToString("MM-dd"),
                            Inbound = data.Inbound,
                            Outbound = data.Outbound
                        };
                    }
                    else
                    {
                        return new
                        {
                            Date = date.ToString("MM-dd"),
                            Inbound = 0,
                            Outbound = 0
                        };
                    }
                })
                .OrderBy(x => x.Date)
                .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每日统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 每周统计
        /// </summary>
        /// <remarks>
        /// 注意:数据在 SQL 层过滤后,在应用层按 ISO 8601 周键分组。
        /// 周键为 "YYYY-Www" 格式,无法直接在 SQL 层用 GROUP BY 实现。
        /// </remarks>
        [HttpGet("WeeklyStats"), AllowAnonymous]
        public async Task<WebResponseContent> WeeklyStats([FromQuery] int weeks = 12)
        {
            try
            {
                if (weeks <= 0) weeks = 12;
                var startDate = DateTime.Today.AddDays(-weeks * 7);
                var query = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .Select(t => new { t.InsertTime, t.TaskType })
                    .ToListAsync();
                var result = query
                    .GroupBy(t => GetWeekKey(t.InsertTime))
                    .Select(g => new
                    {
                        Week = g.Key,
                        Inbound = g.Count(t => t.TaskType >= 200 && t.TaskType < 300),
                        Outbound = g.Count(t => t.TaskType >= 100 && t.TaskType < 200)
                    })
                    .OrderBy(x => x.Week)
                    .ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"每周统计获取失败: {ex.Message}");
            }
        }
        private string GetWeekKey(DateTime date)
        {
            // 获取周一开始的周 (ISO 8601)
            var diff = (7 + (date.DayOfWeek - DayOfWeek.Monday)) % 7;
            var monday = date.AddDays(-diff);
            var weekNum = System.Globalization.CultureInfo.InvariantCulture
                .Calendar.GetWeekOfYear(monday, System.Globalization.CalendarWeekRule.FirstFourDayWeek, DayOfWeek.Monday);
            return $"{monday.Year}-W{weekNum:D2}";
        }
        /// <summary>
        /// 每月统计
        /// </summary>
        /// <remarks>
        /// 按年月统计入站和出站任务数量
        /// </remarks>
        [HttpGet("MonthlyStats"), AllowAnonymous]
        public async Task<WebResponseContent> MonthlyStats([FromQuery] int months = 12)
        {
            try
            {
                if (months <= 0) months = 12;
                var startDate = DateTime.Today.AddMonths(-months + 1);
                startDate = new DateTime(startDate.Year, startDate.Month, 1);
                var monthlyStats = await _db.Queryable<Dt_Task_Hty>()
                    .Where(t => t.InsertTime >= startDate)
                    .GroupBy(t => new { t.InsertTime.Year, t.InsertTime.Month })
                    .Select(t => new
                    {
                        Year = t.InsertTime.Year,
                        Month = t.InsertTime.Month,
                        Inbound = SqlFunc.AggregateSum(
                            SqlFunc.IIF(t.TaskType >= 200 && t.TaskType < 300, 1, 0)
                        ),
                        Outbound = SqlFunc.AggregateSum(
                            SqlFunc.IIF(t.TaskType >= 100 && t.TaskType < 200, 1, 0)
                        )
                    })
                    .OrderBy(t => t.Year)
                    .OrderBy(t => t.Month)
                    .ToListAsync();
                // 生成所有需要统计的月份列表
                var allMonths = new List<DateTime>();
                var currentMonth = startDate;
                var endMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
                while (currentMonth <= endMonth)
                {
                    allMonths.Add(currentMonth);
                    currentMonth = currentMonth.AddMonths(1);
                }
                // 将查询结果转换为字典,方便查找
                var statsDict = monthlyStats.ToDictionary(
                    s => $"{s.Year}-{s.Month:D2}",
                    s => new { s.Inbound, s.Outbound }
                );
                // 构建完整的结果列表,包含所有月份
                var result = new List<object>();
                foreach (var month in allMonths)
                {
                    var monthKey = $"{month.Year}-{month.Month:D2}";
                    if (statsDict.TryGetValue(monthKey, out var stat))
                    {
                        result.Add(new
                        {
                            Month = monthKey,
                            Inbound = stat.Inbound,
                            Outbound = stat.Outbound
                        });
                    }
                    else
                    {
                        result.Add(new
                        {
                            Month = monthKey,
                            Inbound = 0,
                            Outbound = 0
                        });
                    }
                }
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                // 记录异常日志(实际项目中建议使用日志框架)
                // _logger.LogError(ex, "每月统计获取失败");
                return WebResponseContent.Instance.Error($"每月统计获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 库存库龄分布
        /// </summary>
        [HttpGet("StockAgeDistribution"), AllowAnonymous]
        public async Task<WebResponseContent> StockAgeDistribution()
        {
            try
            {
                var today = DateTime.Today;
                // 使用 SQL 直接分组统计,避免加载所有数据到内存
                var result = new[]
                {
                    new { Range = "7天内", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 7).CountAsync() },
                    new { Range = "7-30天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 7 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 30).CountAsync() },
                    new { Range = "30-90天", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 30 && SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) <= 90).CountAsync() },
                    new { Range = "90天以上", Count = await _db.Queryable<Dt_StockInfo>().Where(s => SqlFunc.DateDiff(DateType.Day, s.CreateDate, today) > 90).CountAsync() }
                };
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"库存库龄分布获取失败: {ex.Message}");
            }
        }
        /// <summary>
        /// 各仓库库存分布
        /// </summary>
        /// <remarks>
        /// 使用 SQL GROUP BY 在数据库层面聚合,避免加载全部库存记录到内存。
        /// </remarks>
        [HttpGet("StockByWarehouse"), AllowAnonymous]
        public async Task<WebResponseContent> StockByWarehouse()
        {
            try
            {
                // 查询所有仓库信息
                var warehouses = await _db.Queryable<Dt_Warehouse>()
                    .Select(w => new { w.WarehouseId, w.WarehouseName })
                    .ToListAsync();
                // 查询所有货位信息,按仓库分组统计总数
                var locationGroups = await _db.Queryable<Dt_LocationInfo>()
                    .GroupBy(l => l.WarehouseId)
                    .Select(l => new
                    {
                        WarehouseId = l.WarehouseId,
                        TotalLocations = SqlFunc.AggregateCount(l.Id)
                    })
                    .ToListAsync();
                // 查询状态不为Free的货位信息(有货货位),按仓库分组统计
                var occupiedLocationGroups = await _db.Queryable<Dt_LocationInfo>()
                    .Where(l => l.LocationStatus != (int)LocationStatusEnum.Free)
                    .GroupBy(l => l.WarehouseId)
                    .Select(l => new
                    {
                        WarehouseId = l.WarehouseId,
                        OccupiedLocations = SqlFunc.AggregateCount(l.Id)
                    })
                    .ToListAsync();
                // 将仓库信息与货位统计信息合并
                var result = warehouses.Select(w =>
                {
                    var totalLocations = locationGroups.FirstOrDefault(lg => lg.WarehouseId == w.WarehouseId)?.TotalLocations ?? 0;
                    var occupiedLocations = occupiedLocationGroups.FirstOrDefault(og => og.WarehouseId == w.WarehouseId)?.OccupiedLocations ?? 0;
                    var emptyLocations = totalLocations - occupiedLocations;
                    var occupiedPercentage = totalLocations > 0 ? Math.Round((double)occupiedLocations / totalLocations * 100, 2) : 0.0;
                    var emptyPercentage = totalLocations > 0 ? Math.Round((double)emptyLocations / totalLocations * 100, 2) : 0.0;
                    return new
                    {
                        Warehouse = w.WarehouseName,
                        Total = totalLocations,
                        HasStock = occupiedLocations,
                        NoStock = emptyLocations,
                        HasStockPercentage = $"{occupiedPercentage}%",
                        NoStockPercentage = $"{emptyPercentage}%"
                    };
                }).ToList();
                return WebResponseContent.Instance.OK(null, result);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error($"各仓库库存分布获取失败: {ex.Message}");
            }
        }
    }
}