647556386
20 小时以前 b680585c3a6d43f0c72a83a115ea537ce8c91a07
Merge branch 'master' of http://115.159.85.185:8098/r/ZhongRui/ALDbanyunxiangmu
已添加3个文件
已修改6个文件
958 ■■■■■ 文件已修改
项目代码/WIDESEA_WMSClient/src/extension/inbound/extend/EmptyTrayInbound.vue 96 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/router/viewGird.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/basic/locationInfo.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/stock/stockView.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WIDESEA_WMSClient/src/views/system/Log.vue 346 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目代码/WMS无仓储版/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/System/Sys_LogController.cs 384 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/数据库字典/ALD_20251212105252.pdf 补丁 | 查看 | 原始文档 | blame | 历史
项目资料/数据库字典/WIDESEAWMS_ALDZhongRui.sql 补丁 | 查看 | 原始文档 | blame | 历史
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/extension/inbound/extend/EmptyTrayInbound.vue
@@ -1,17 +1,17 @@
<template>
    <vol-box v-model="show" title="空托入库" :width="800" :height="1200">
        <template #content>
            <el-form ref="form" :model="form" label-width="90px">
                <el-form-item label="入库区域:">
            <el-form ref="form" :model="form" :rules="rules" label-width="90px">
                <el-form-item label="入库区域:" prop="locationType">
                    <el-select v-model="form.locationType" placeholder="请选择入库区域">
                        <el-option v-for="item in locationTypes" :key="item.locationType" :label="item.locationTypeDesc"
                            :value="item.locationType" />
                    </el-select>
                </el-form-item>
                <el-form-item label="托盘条码:">
                    <el-input v-model="form.palletCode" placeholder="请扫描/输入托盘条码" @keyup.enter="submit" @keyup.13="submit"
                        clearable maxlength="50" @paste="handlePaste" @input="handleInput" ref="boxCodeInput" />
                <el-form-item label="托盘条码:" prop="palletCode">
                    <el-input v-model="form.palletCode" placeholder="请扫描/输入托盘条码(A开头,后跟数字)" @keyup.enter="submit" clearable
                        @paste="handlePaste" @input="handleInput" ref="boxCodeInput" />
                </el-form-item>
            </el-form>
        </template>
@@ -34,13 +34,37 @@
        value: { type: Boolean, default: false }
    },
    data() {
        // è‡ªå®šä¹‰æ¡ç éªŒè¯è§„则
        const validatePalletCode = (rule, value, callback) => {
            if (!value) {
                return callback(new Error('请输入托盘条码'));
            }
            // éªŒè¯æ¡ç æ ¼å¼ï¼šA开头,后面至少1位数字(不限制具体长度)
            const codePattern = /^A\d+$/;
            if (!codePattern.test(value)) {
                return callback(new Error('条码格式不正确!正确格式:A开头,后跟数字,如:A000008080'));
            }
            callback();
        };
        return {
            show: false,
            form: {
                palletCode: '',
                locationType: ''
            },
            locationTypes: []
            locationTypes: [],
            // è¡¨å•验证规则
            rules: {
                locationType: [
                    { required: true, message: '请选择入库区域', trigger: 'change' }
                ],
                palletCode: [
                    { validator: validatePalletCode, trigger: ['blur', 'change'] }
                ]
            }
        }
    },
    methods: {
@@ -62,15 +86,13 @@
        },
        async submit() {
            if (!this.form.palletCode) {
                this.$message.warning('请输入托盘条码')
                this.focusInput()
                return
            }
            if (!this.form.locationType) {
                this.$message.warning('请选择入库区域')
                return
            // è¡¨å•验证
            try {
                await this.$refs.form.validate();
            } catch (error) {
                // éªŒè¯å¤±è´¥ï¼Œèšç„¦è¾“入框
                this.focusAndSelectInput();
                return;
            }
            try {
@@ -88,6 +110,8 @@
                    this.$message.success("组盘成功");
                    // æ¸…空输入框数据
                    this.form.palletCode = '';
                    // é‡ç½®éªŒè¯çŠ¶æ€
                    this.$refs.form.clearValidate('palletCode');
                    // èšç„¦å¹¶é€‰ä¸­è¾“入框
                    this.focusAndSelectInput();
                } else {
@@ -104,15 +128,39 @@
        // æ‰«ææžªä¼˜åŒ–处理
        handleInput(value) {
            // è¿‡æ»¤éžæ•°å­—和条码常用字符
            this.form.palletCode = value.replace(/[^a-zA-Z0-9\-]/g, '')
            // è¿‡æ»¤éžæ•°å­—和条码常用字符,允许A开头
            this.form.palletCode = value.replace(/[^a-zA-Z0-9]/g, '')
            // è‡ªåŠ¨è½¬æ¢ä¸ºå¤§å†™ï¼ˆæ¡ç é€šå¸¸ä¸ºå¤§å†™ï¼‰
            this.form.palletCode = this.form.palletCode.toUpperCase();
            // è‡ªåŠ¨è§¦å‘éªŒè¯
            this.$nextTick(() => {
                this.$refs.form.validateField('palletCode');
            });
        },
        handlePaste(e) {
            // ç²˜è´´æ—¶è‡ªåŠ¨æäº¤
            setTimeout(this.submit, 100)
            // èŽ·å–ç²˜è´´çš„å†…å®¹
            const clipboardData = e.clipboardData || window.clipboardData;
            const pastedText = clipboardData.getData('text');
            // å¤„理粘贴内容
            const cleanedText = pastedText.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
            // å¦‚果粘贴内容符合条码格式,自动填充并提交
            if (cleanedText.startsWith('A')) {
                this.form.palletCode = cleanedText;
                // å»¶è¿Ÿæäº¤ï¼Œç¡®ä¿è¡¨å•已更新
                setTimeout(() => {
                    this.submit();
                }, 50);
            }
            // é˜»æ­¢é»˜è®¤ç²˜è´´è¡Œä¸ºï¼Œä½¿ç”¨æˆ‘们处理后的值
            e.preventDefault();
        },
        // èšç„¦å¹¶é€‰ä¸­è¾“入框
        focusAndSelectInput() {
            this.$nextTick(() => {
@@ -129,7 +177,7 @@
                }, 100);
            });
        },
        // åªèšç„¦è¾“入框(不清空数据)
        focusInput() {
            this.$nextTick(() => {
@@ -140,10 +188,14 @@
                }
            });
        },
        // æ¸…空表单数据
        clearForm() {
            this.form.palletCode = '';
            // é‡ç½®éªŒè¯çŠ¶æ€
            if (this.$refs.form) {
                this.$refs.form.clearValidate();
            }
            // ä¸æ¸…空 locationType,保持区域选择
        }
    },
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/router/viewGird.js
@@ -227,7 +227,13 @@
    path:'/printForm',
    name: 'printForm',
    component: () => import('@/views/outbound/printForm.vue') 
  },
  },{
    path: '/Log',
    name: 'Log',
    component: () => import('@/views/system/Log.vue'),
    meta: {
    }
  }
]
export default viewgird   
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/basic/locationInfo.vue
@@ -48,7 +48,7 @@
      [
        { title: "货位编号", field: "locationCode", type: "like" },
        { title: "巷道编号", field: "roadwayNo",type:"like" },
        { title: "货位类型", field: "locationType",type: "select",dataKey: "locationTypeEnum",data: [], },
        { title: "货位区域", field: "locationType",type: "select",dataKey: "locationTypeEnum",data: [], },
        { title: "禁用状态", field: "enableStatus" ,type: "select",dataKey: "enableStatusEnum",data: [],},
      ],
      [
@@ -132,7 +132,7 @@
      },
      {
        field: "locationType",
        title: "货位类型",
        title: "货位区域",
        type: "string",
        width: 120,
        align: "left",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/stock/stockView.vue
@@ -158,7 +158,7 @@
      },
      {
        field: "locationType",
        title: "货位类型",
        title: "货位区域",
        type: "string",
        width: 140,
        align: "left",
ÏîÄ¿´úÂë/WIDESEA_WMSClient/src/views/system/Log.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,346 @@
<template>
  <div class="tree-container">
    <el-input placeholder="输入关键字进行过滤" v-model="filterText" class="filter-input">
    </el-input>
    <div class="custom-tree-wrapper">
      <el-tree ref="tree" class="filter-tree" :filter-node-method="filterNode" :data="data" node-key="id" accordion>
        <template #default="{ node, data }">
          <div class="custom-tree-node">
            <span class="node-label">{{ node.label }}</span>
            <span v-if="data.hidden" class="node-actions">
              <el-button type="text" size="mini" @click="() => view(data)" class="action-btn">
                æŸ¥çœ‹
              </el-button>
              <el-button type="text" size="mini" @click="() => dowmload(node, data)" class="action-btn">
                ä¸‹è½½
              </el-button>
            </span>
          </div>
        </template>
      </el-tree>
    </div>
  </div>
  <div class="log-container">
    <el-card shadow="always" v-if="logName" class="log-card">
      <template #header>
        <div class="card-header">
          <el-tag type="info" size="small">日志文件</el-tag>
          <span class="log-title">{{ logName }}</span>
        </div>
      </template>
      <div class="log-content">
        <div v-for="(item, index) in log" :key="index" class="log-line">
          <span class="line-number">{{ index + 1 }}</span>
          <span class="line-content">{{ item }}</span>
        </div>
      </div>
    </el-card>
    <div v-else class="empty-log">
      <el-empty description="请选择日志文件进行查看" />
    </div>
  </div>
</template>
<script>
export default {
  data() {
    return {
      data: [],
      defaultProps: {
        children: "children",
        label: "label",
      },
      filterText: "",
      logName: "",
      log: [],
    };
  },
  watch: {
    filterText(val) {
      this.$refs.tree.filter(val);
    },
  },
  created() {
    this.getLogName();
  },
  methods: {
    filterNode(value, data) {
      if (!value) return true;
      return data.label.indexOf(value) !== -1;
    },
    getLogName() {
      this.http
        .post("/api/Sys_Log/GetLogName", null, "正在执行中...")
        .then((x) => {
          if (x.status) {
            this.data = x.data;
          }
        });
    },
    view(data) {
      // var params = {
      //   MainData: { fileName: data.label },
      // };
      this.http
        .post("/api/Sys_Log/GetLog?fileName=" + data.label, "正在查询中...")
        .then((x) => {
          if (x.status) {
            this.logName = data.label;
            this.log = x.data;
          }
        });
    },
    dowmload(node, data) {
      let ipAddress = this.http.ipAddress;
      let url =
        "api/Sys_Log/DownLoadLog?fileName=" +
        data.fatherNode +
        "\\" +
        data.label;
      let fileName = data.label;
      let xmlResquest = new XMLHttpRequest();
      xmlResquest.open("GET", ipAddress + url, true);
      xmlResquest.setRequestHeader("Content-type", "application/json");
      xmlResquest.setRequestHeader(
        "Authorization",
        this.$store.getters.getToken()
      );
      let elink = this.$refs.template;
      xmlResquest.responseType = "blob";
      let $_vue = this;
      this.loadingStatus = true;
      xmlResquest.onload = function (e) {
        // è¯·æ±‚成功
        if (this.status == 200) {
          let blob = this.response;
          let a = document.createElement("a");
          //window.URL.createObjectURL() é™æ€æ–¹æ³•会创建一个 DOMString,其中包含一个表示参数中给出的对象的URL。这个 URL çš„生命周期和创建它的窗口中的 document ç»‘定。这个新的URL å¯¹è±¡è¡¨ç¤ºæŒ‡å®šçš„ File å¯¹è±¡æˆ– Blob å¯¹è±¡ã€‚
          let url = window.URL.createObjectURL(blob);
          a.href = url;
          a.download = fileName;
          a.click();
          //URL.revokeObjectURL() é™æ€æ–¹æ³•用来释放一个之前已经存在的、通过调用 URL.createObjectURL() åˆ›å»ºçš„ URL å¯¹è±¡ã€‚当你结束使用某个 URL å¯¹è±¡ä¹‹åŽï¼Œåº”该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了。
          window.URL.revokeObjectURL(url);
        } else {
          //下载失败处理
        }
      };
      xmlResquest.send();
    },
  },
};
</script>
<style scoped>
.tree-container {
  width: 30%;
  float: left;
  padding: 16px;
  background: #f8f9fa;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.filter-input {
  margin-bottom: 16px;
}
.filter-input :deep(.el-input__inner) {
  border-radius: 20px;
  border-color: #dcdfe6;
  transition: all 0.3s;
}
.filter-input :deep(.el-input__inner):focus {
  border-color: #409eff;
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
}
.custom-tree-wrapper {
  height: 760px;
  overflow-y: auto;
  padding: 8px;
  background: white;
  border-radius: 6px;
  border: 1px solid #ebeef5;
}
.filter-tree :deep(.el-tree-node__content) {
  height: 40px;
  margin: 2px 0;
  border-radius: 4px;
  transition: all 0.2s;
}
.filter-tree :deep(.el-tree-node__content:hover) {
  background-color: #f0f9ff;
}
.filter-tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
  background-color: #ecf5ff;
  font-weight: 600;
}
.custom-tree-node {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0 8px;
}
.node-label {
  font-size: 14px;
  color: #606266;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.node-actions {
  display: flex;
  gap: 8px;
  margin-left: 12px;
}
.action-btn {
  padding: 4px 8px;
  font-size: 12px;
  color: #409eff;
  border-radius: 3px;
}
.action-btn:hover {
  background-color: rgba(64, 158, 255, 0.1);
}
.log-container {
  width: 68%;
  float: right;
  padding: 16px;
}
.log-card {
  height: 800px;
  border-radius: 8px;
  border: 1px solid #ebeef5;
}
.log-card :deep(.el-card__header) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-bottom: 1px solid #ebeef5;
  padding: 16px 20px;
}
.card-header {
  display: flex;
  align-items: center;
  gap: 12px;
}
.log-title {
  font-size: 16px;
  font-weight: 600;
  color: white;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.log-content {
  height: 700px;
  overflow-y: auto;
  font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
  background: #f8f9fa;
  padding: 12px;
  border-radius: 4px;
}
.log-line {
  display: flex;
  align-items: flex-start;
  margin-bottom: 4px;
  line-height: 1.5;
  background: white;
  padding: 8px 12px;
  border-radius: 4px;
  border-left: 3px solid #409eff;
  transition: all 0.2s;
}
.log-line:hover {
  background: #f0f9ff;
  transform: translateX(2px);
}
.line-number {
  display: inline-block;
  min-width: 40px;
  padding-right: 12px;
  text-align: right;
  color: #909399;
  font-size: 12px;
  user-select: none;
}
.line-content {
  flex: 1;
  color: #303133;
  font-size: 13px;
  word-break: break-all;
  white-space: pre-wrap;
}
.empty-log {
  height: 800px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: white;
  border-radius: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.empty-log :deep(.el-empty__description) {
  margin-top: 8px;
}
/* æ»šåŠ¨æ¡æ ·å¼ */
.custom-tree-wrapper::-webkit-scrollbar,
.log-content::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
.custom-tree-wrapper::-webkit-scrollbar-track,
.log-content::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 3px;
}
.custom-tree-wrapper::-webkit-scrollbar-thumb,
.log-content::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 3px;
}
.custom-tree-wrapper::-webkit-scrollbar-thumb:hover,
.log-content::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}
/* æ¸…除浮动 */
.tree-container::after,
.log-container::after {
  content: "";
  display: table;
  clear: both;
}
</style>
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_TaskInfoService/TaskService.cs
@@ -714,7 +714,7 @@
                    _logger.LogInformation($"InPickTaskCompleted AddLocationStatusChangeRecord : {ex.Message} ");
                }
                _logger.LogInformation($"托盘回库完成处理成功 - ä»»åŠ¡å·: {task.TaskNum}, æ‰˜ç›˜: {task.PalletCode}, è®¢å•: {task.OrderNo}");
                _logger.LogInformation($"托盘回库完成处理成功 - ä»»åŠ¡å·: {task.TaskNum}, æ‰˜ç›˜: {task.PalletCode}, è®¢å•: {task.OrderNo} è´§ä½çŠ¶æ€ï¼š{locationInfo.LocationStatus}");
                _ = Task.Run(async () =>
                {
                    try
@@ -1136,33 +1136,36 @@
                                allocatefeedmodel.Details.Add(detailModel);
                            }
                            var groupedResult = allocatefeedmodel.Details.GroupBy(item => new
                            {
                                item.WarehouseCode,
                                item.MaterialCode,
                                item.Unit,
                                item.LineNo
                            }).Select(group => new AllocateDtoDetail
                            {
                                 WarehouseCode = group.Key.WarehouseCode,
                                 MaterialCode = group.Key.MaterialCode,
                                 LineNo = group.Key.LineNo,
                                 Qty = group.Sum(x => x.Qty),
                                 Unit = group.Key.Unit,
                                 Barcodes = group.SelectMany(x => x.Barcodes)
                                                       .GroupBy(b => b.Barcode)
                                                       .Select(b => new BarcodeInfo
                                                       {
                                                           Barcode = b.Key,
                                                           BatchNo = b.First().BatchNo,
                                                           SupplyCode = b.First().SupplyCode,
                                                           Qty = b.Max(x => x.Qty),
                                                           Unit = b.First().Unit
                                                       }) .ToList()
                             }) .ToList();
                            var groupedResult = allocatefeedmodel.Details
                                .GroupBy(item => new { item.WarehouseCode, item.MaterialCode, item.Unit, item.LineNo })
                                .Select(group =>
                                {
                                    var deduplicatedBarcodes = group.SelectMany(x => x.Barcodes)
                                                                   .GroupBy(b => b.Barcode)
                                                                   .Select(b => new BarcodeInfo
                                                                   {
                                                                       Barcode = b.Key,
                                                                       BatchNo = b.First().BatchNo,
                                                                       SupplyCode = b.First().SupplyCode,
                                                                       Qty = b.Max(x => x.Qty),
                                                                       Unit = b.First().Unit
                                                                   }).ToList();
                                    return new AllocateDtoDetail
                                    {
                                        WarehouseCode = group.Key.WarehouseCode,
                                        MaterialCode = group.Key.MaterialCode,
                                        LineNo = group.Key.LineNo,
                                        Qty = deduplicatedBarcodes.Sum(b => b.Qty),
                                        Unit = group.Key.Unit,
                                        Barcodes = deduplicatedBarcodes
                                    };
                                }).ToList();
                            allocatefeedmodel.Details = groupedResult;
                          var result = await _invokeMESService.FeedbackAllocate(allocatefeedmodel);
                            var result = await _invokeMESService.FeedbackAllocate(allocatefeedmodel);
                            if (result != null && result.code == 200)
                            {
                                await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
@@ -1175,6 +1178,19 @@
                                          ReturnToMESStatus = 1,
                                      }).Where(x => x.OrderNo == orderNo).ExecuteCommandAsync();
                            }
                            else
                            {
                                await _outboundOrderDetailService.Db.Updateable<Dt_OutboundOrderDetail>()
                                 .SetColumns(x => x.ReturnToMESStatus == 2)
                                 .Where(x => x.OrderId == outboundOrder.Id)
                                 .ExecuteCommandAsync();
                                await _outboundOrderService.Db.Updateable<Dt_OutboundOrder>()
                                    .SetColumns(it => new Dt_OutboundOrder { ReturnToMESStatus = 2, Remark = result.message })
                                     .Where(x => x.OrderNo == orderNo)
                                    .ExecuteCommandAsync();
                            }
                        }
                    }
                    else if (outboundOrder.OrderType == OutOrderTypeEnum.ReCheck.ObjToInt())
@@ -1247,30 +1263,32 @@
                                }
                                feedmodel.details.Add(detailModel);
                            }
                            var groupedResult = feedmodel.details.GroupBy(item => new
                            {
                                item.warehouseCode,
                                item.materialCode,
                                item.unit,
                                item.lineNo
                            }).Select(group => new FeedbackOutboundDetailsModel
                            {
                                warehouseCode = group.Key.warehouseCode,
                                materialCode = group.Key.materialCode,
                                lineNo = group.Key.lineNo,
                                qty = group.Sum(x => x.qty),
                                unit = group.Key.unit,
                                barcodes = group.SelectMany(x => x.barcodes)
                                                       .GroupBy(b => b.barcode)
                                                       .Select(b => new WIDESEA_DTO.Outbound.BarcodesModel
                                                       {
                                                           barcode = b.Key,
                                                           batchNo = b.First().batchNo,
                                                           supplyCode = b.First().supplyCode,
                                                           qty = b.Max(x => x.qty),
                                                           unit = b.First().unit
                                                       }).ToList()
                            }).ToList();
                            var groupedResult = feedmodel.details
                               .GroupBy(item => new { item.warehouseCode, item.materialCode, item.unit, item.lineNo })
                               .Select(group =>
                               {
                                   var deduplicatedBarcodes = group.SelectMany(x => x.barcodes)
                                                                  .GroupBy(b => b.barcode)
                                                                  .Select(b => new WIDESEA_DTO.Outbound.BarcodesModel
                                                                  {
                                                                      barcode = b.Key,
                                                                      batchNo = b.First().batchNo,
                                                                      supplyCode = b.First().supplyCode,
                                                                      qty = b.Max(x => x.qty),
                                                                      unit = b.First().unit
                                                                  }).ToList();
                                   return new FeedbackOutboundDetailsModel
                                   {
                                       warehouseCode = group.Key.warehouseCode,
                                       materialCode = group.Key.materialCode,
                                       lineNo = group.Key.lineNo,
                                       qty = deduplicatedBarcodes.Sum(b => b.qty),
                                       unit = group.Key.unit,
                                       barcodes = deduplicatedBarcodes
                                   };
                               }).ToList();
                            feedmodel.details = groupedResult;
                            var result = await _invokeMESService.FeedbackOutbound(feedmodel);
ÏîÄ¿´úÂë/WMSÎÞ²Ö´¢°æ/WIDESEA_WMSServer/WIDESEA_WMSServer/Controllers/System/Sys_LogController.cs
@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.Text;
using WIDESEA_Core;
using WIDESEA_Core.BaseController;
using WIDESEA_ISystemService;
using WIDESEA_Model.Models;
@@ -13,8 +16,385 @@
    [ApiController]
    public class Sys_LogController : ApiBaseController<ISys_LogService, Sys_Log>
    {
        // é…ç½®å¸¸é‡
        private const int MAX_FILE_SIZE_MB = 50;
        private const int MAX_RETRY_COUNT = 3;
        private const int RETRY_DELAY_MS = 100;
        private static readonly string[] ALLOWED_FILE_TYPES = { ".txt", ".log", ".csv", ".json", ".xml" };
        public Sys_LogController(ISys_LogService service) : base(service)
        {
        }
        [HttpPost, Route("GetLogName"), AllowAnonymous]
        public WebResponseContent GetLogName()
        {
            WebResponseContent content = new WebResponseContent();
            try
            {
                List<object> data = new List<object>();
                DirectoryInfo folder = new DirectoryInfo(AppContext.BaseDirectory + "\\logs\\");
                DirectoryInfo[] firstDirectoryInfos = folder.GetDirectories().OrderByDescending(x => x.CreationTime).ToArray();
                int k = 2020;
                for (int i = 0; i < firstDirectoryInfos.Length; i++)
                {
                    if (firstDirectoryInfos[i].Name != "Info")
                    {
                        FileInfo[] nextFileInfos = firstDirectoryInfos[i].GetFiles();
                        List<object> values = new List<object>();
                        for (int j = 0; j < nextFileInfos.Length; j++)
                        {
                            values.Add(new { label = nextFileInfos[j].Name, id = k, hidden = true, fatherNode = firstDirectoryInfos[i].Name });
                            k++;
                        }
                        data.Add(new { label = firstDirectoryInfos[i].Name, children = values, id = i, hidden = false });
                    }
                }
                FileInfo[] nextFileInfo = folder.GetFiles();
                List<object> value = new List<object>();
                for (int j = 0; j < nextFileInfo.Length; j++)
                {
                    value.Add(new { label = nextFileInfo[j].Name, id = k, hidden = true, fatherNode = folder.Name });
                    k++;
                }
                data.Add(new { label = folder.Name, children = value, id = 1, hidden = false });
                return WebResponseContent.Instance.OK(data: data);
            }
            catch (Exception ex)
            {
                return WebResponseContent.Instance.Error(ex.Message);
            }
        }
        [HttpPost, Route("GetLog"), AllowAnonymous]
        public WebResponseContent GetLog(string fileName)
        {
            WebResponseContent content = new WebResponseContent();
            try
            {
                List<FileInfo> files = new List<FileInfo>();
                DirectoryInfo folder = new DirectoryInfo(AppContext.BaseDirectory + "\\logs\\");
                DirectoryInfo[] firstDirectoryInfos = folder.GetDirectories();
                for (int i = 0; i < firstDirectoryInfos.Length; i++)
                {
                    FileInfo[] nextFileInfos = firstDirectoryInfos[i].GetFiles();
                    files.AddRange(nextFileInfos);
                }
                FileInfo[] nextFileInfo = folder.GetFiles();
                files.AddRange(nextFileInfo);
                if (files.Count > 0)
                {
                    FileInfo file = files.Where(x => x.Name == fileName).FirstOrDefault();
                    if (file == null)
                    {
                        return WebResponseContent.Instance.Error($"未找到日志文件: {fileName}");
                    }
                    // ä½¿ç”¨å…±äº«è¯»å–模式
                    using (FileStream stream = new FileStream(
                        file.FullName,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.ReadWrite))
                    using (StreamReader reader = new StreamReader(stream, Encoding.UTF8))
                    {
                        StringBuilder text = new StringBuilder();
                        List<string> lines = new List<string>();
                        while (!reader.EndOfStream)
                        {
                            var line = reader.ReadLine();
                            lines.Add(line);
                        }
                        content = WebResponseContent.Instance.OK(data: lines);
                    }
                }
                else
                {
                    content = WebResponseContent.Instance.Error($"未找到日志文件,【{fileName}】");
                }
            }
            catch (IOException ex)
            {
                if (IsFileLockedException(ex))
                {
                    content = WebResponseContent.Instance.Error($"日志文件正在被系统写入,请稍后再试");
                }
                else
                {
                    content = WebResponseContent.Instance.Error($"打开日志文件错误,{ex.Message}");
                }
            }
            catch (Exception ex)
            {
                content = WebResponseContent.Instance.Error($"打开日志文件错误,{ex.Message}");
            }
            return content;
        }
        [HttpPost, HttpGet, Route("DownLoadLog"), AllowAnonymous]
        public virtual async Task<ActionResult> DownLoadLog(string fileName)
        {
            try
            {
                // 1. å‚数验证
                if (string.IsNullOrWhiteSpace(fileName))
                {
                    return BadRequest("文件名不能为空");
                }
                // å®‰å…¨æ€§æ£€æŸ¥ï¼šé˜²æ­¢è·¯å¾„遍历攻击
                if (fileName.Contains("..") || Path.IsPathRooted(fileName))
                {
                    return BadRequest("无效的文件名");
                }
                //string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
                string logDirectory = Path.Combine(AppContext.BaseDirectory);
                if (!Directory.Exists(logDirectory))
                {
                    Directory.CreateDirectory(logDirectory);
                }
                string filePath = Path.Combine(logDirectory, fileName);
                if (Directory.Exists(filePath))
                {
                    return NotFound($"文件 {fileName} ä¸å­˜åœ¨");
                }
                string extension = Path.GetExtension(fileName).ToLowerInvariant();
                if (!IsAllowedFileType(extension))
                {
                    return BadRequest($"不支持的文件类型: {extension}");
                }
                FileInfo fileInfo = new FileInfo(filePath);
                if (fileInfo.Length > MAX_FILE_SIZE_MB * 1024 * 1024)
                {
                    return BadRequest($"文件过大,超过{MAX_FILE_SIZE_MB}MB限制");
                }
                // æ–¹æ¡ˆ1:使用重试机制 + å…±äº«è¯»å–(推荐)
                byte[] fileBytes = await ReadFileWithRetryAsync(filePath);
                if (fileBytes == null)
                {
                    return StatusCode(500, "文件被占用,无法下载,请稍后重试");
                }
                string contentType = GetContentType(extension);
                // è®¾ç½®ä¸‹è½½å¤´
                Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{System.Web.HttpUtility.UrlEncode(fileName, Encoding.UTF8)}\"");
                Response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate");
                Response.Headers.Add("Pragma", "no-cache");
                Response.Headers.Add("Expires", "0");
                return File(fileBytes, contentType, fileName);
            }
            catch (UnauthorizedAccessException)
            {
                return StatusCode(403, "没有访问该文件的权限");
            }
            catch (PathTooLongException)
            {
                return BadRequest("文件路径过长");
            }
            catch (IOException ex)
            {
                if (IsFileLockedException(ex))
                {
                    return StatusCode(500, "文件被锁定,可能正在被系统写入,请稍后再试");
                }
                return StatusCode(500, $"文件读取失败: {ex.Message}");
            }
            catch (Exception ex)
            {
                // è®°å½•异常日志(这里简化为返回,实际项目中应该记录到日志系统)
                return StatusCode(500, $"服务器内部错误: {ex.Message}");
            }
        }
        /// <summary>
        /// å¸¦é‡è¯•机制的文件读取方法
        /// </summary>
        private async Task<byte[]> ReadFileWithRetryAsync(string filePath)
        {
            for (int attempt = 0; attempt < MAX_RETRY_COUNT; attempt++)
            {
                try
                {
                    // ä½¿ç”¨ FileShare.ReadWrite å…è®¸å…¶ä»–进程同时读取和写入
                    // ä½¿ç”¨å¼‚步读取提高性能
                    using (var fileStream = new FileStream(
                        filePath,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.ReadWrite | FileShare.Delete, // å…è®¸åˆ é™¤æ“ä½œ
                        bufferSize: 4096,
                        useAsync: true))
                    {
                        using (var memoryStream = new MemoryStream())
                        {
                            await fileStream.CopyToAsync(memoryStream);
                            return memoryStream.ToArray();
                        }
                    }
                }
                catch (IOException) when (attempt < MAX_RETRY_COUNT - 1)
                {
                    // å¦‚果不是最后一次重试,等待一段时间
                    await Task.Delay(RETRY_DELAY_MS * (attempt + 1));
                }
                catch (IOException ex)
                {
                    // æœ€åŽä¸€æ¬¡å°è¯•也失败了
                    throw;
                }
            }
            return null;
        }
        /// <summary>
        /// åˆ¤æ–­æ˜¯å¦ä¸ºæ–‡ä»¶è¢«é”å®šçš„异常
        /// </summary>
        private bool IsFileLockedException(IOException ex)
        {
            int errorCode = ex.HResult & 0xFFFF;
            return errorCode == 32 || errorCode == 33; // ERROR_SHARING_VIOLATION or ERROR_LOCK_VIOLATION
        }
        /// <summary>
        /// æ£€æŸ¥æ–‡ä»¶ç±»åž‹æ˜¯å¦å…è®¸
        /// </summary>
        private bool IsAllowedFileType(string extension)
        {
            return ALLOWED_FILE_TYPES.Contains(extension);
        }
        /// <summary>
        /// èŽ·å–Content-Type
        /// </summary>
        private string GetContentType(string extension)
        {
            return extension.ToLowerInvariant() switch
            {
                ".txt" => "text/plain; charset=utf-8",
                ".log" => "text/plain; charset=utf-8",
                ".csv" => "text/csv; charset=utf-8",
                ".json" => "application/json; charset=utf-8",
                ".xml" => "application/xml; charset=utf-8",
                _ => "application/octet-stream"
            };
        }
        /// <summary>
        /// å¤‡é€‰æ–¹æ¡ˆï¼šåˆ›å»ºä¸´æ—¶å‰¯æœ¬ä¸‹è½½ï¼ˆæœ€å®‰å…¨ï¼Œä½†æ€§èƒ½ç¨å·®ï¼‰
        /// </summary>
        [HttpPost, HttpGet, Route("DownLoadLogCopy"), AllowAnonymous]
        public virtual async Task<ActionResult> DownLoadLogCopy(string fileName)
        {
            try
            {
                // å‚数验证(同上)
                if (string.IsNullOrWhiteSpace(fileName))
                {
                    return BadRequest("文件名不能为空");
                }
                string logDirectory = Path.Combine(AppContext.BaseDirectory, "logs");
                string filePath = Path.Combine(logDirectory, fileName);
                if (Directory.Exists(filePath))
                {
                    return NotFound($"文件 {fileName} ä¸å­˜åœ¨");
                }
                // ç”Ÿæˆä¸´æ—¶æ–‡ä»¶å
                string tempFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_{Guid.NewGuid():N}{Path.GetExtension(fileName)}";
                string tempFilePath = Path.Combine(Path.GetTempPath(), tempFileName);
                try
                {
                    // å°è¯•复制文件到临时位置(使用重试机制)
                    bool copySuccess = false;
                    for (int attempt = 0; attempt < MAX_RETRY_COUNT; attempt++)
                    {
                        try
                        {
                            //Directory.GetFiles.Copy(filePath, tempFilePath, false);
                            copySuccess = true;
                            break;
                        }
                        catch (IOException) when (attempt < MAX_RETRY_COUNT - 1)
                        {
                            await Task.Delay(RETRY_DELAY_MS * (attempt + 1));
                        }
                    }
                    if (!copySuccess)
                    {
                        return StatusCode(500, "无法复制文件,可能被其他进程占用");
                    }
                    // ä»Žä¸´æ—¶æ–‡ä»¶è¯»å–
                    byte[] fileBytes;
                    using (FileStream tempStream = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        using (MemoryStream memoryStream = new MemoryStream())
                        {
                            await tempStream.CopyToAsync(memoryStream);
                            fileBytes = memoryStream.ToArray();
                        }
                    }
                    string extension = Path.GetExtension(fileName).ToLowerInvariant();
                    string contentType = GetContentType(extension);
                    // è¿”回文件后清理临时文件
                    var result = File(fileBytes, contentType, fileName);
                    // å¼‚步清理临时文件
                    _ = Task.Run(() =>
                    {
                        try
                        {
                            Directory.Delete(tempFilePath);
                        }
                        catch
                        {
                            // å¿½ç•¥åˆ é™¤å¤±è´¥
                        }
                    });
                    return result;
                }
                finally
                {
                    // ç¡®ä¿ä¸´æ—¶æ–‡ä»¶è¢«æ¸…理
                    if (Directory.Exists(tempFilePath))
                    {
                        try
                        {
                            Directory.Delete(tempFilePath);
                        }
                        catch
                        {
                            // å¿½ç•¥åˆ é™¤å¤±è´¥
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                return StatusCode(500, $"服务器内部错误: {ex.Message}");
            }
        }
    }
}
}
ÏîÄ¿×ÊÁÏ/Êý¾Ý¿â×Öµä/ALD_20251212105252.pdf
Binary files differ
ÏîÄ¿×ÊÁÏ/Êý¾Ý¿â×Öµä/WIDESEAWMS_ALDZhongRui.sql
Binary files differ