From 088e1ba6624c7523ee2566110b2c4721a37204a5 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 09 四月 2026 14:22:09 +0800
Subject: [PATCH] #生成波次

---
 rsf-open-api/src/main/resources/application-prod.yml                                         |   17 ++--
 rsf-server/src/main/resources/application-prod.yml                                           |   15 ++-
 rsf-design/.env.development                                                                  |    2 
 rsf-open-api/src/main/resources/application-dev.yml                                          |   11 +-
 .claude/settings.local.json                                                                  |    5 +
 rsf-design/src/locales/langs/zh.json                                                         |    6 +
 rsf-server/src/main/java/com/vincent/rsf/server/common/exception/GlobalExceptionHandler.java |   43 ++++++++++
 rsf-design/src/api/wave.js                                                                   |    7 +
 rsf-design/src/views/orders/out-stock/index.vue                                              |   73 ++++++++++++++---
 rsf-design/src/views/orders/out-stock/outStockPage.helpers.js                                |   22 +++++
 rsf-server/src/main/resources/application-dev.yml                                            |    9 +-
 11 files changed, 169 insertions(+), 41 deletions(-)

diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 6683a8f..0bb830b 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -11,7 +11,10 @@
       "Read(//d/Program Files/Git/bin/**)",
       "Read(//d/Program Files/Git/usr/bin/**)",
       "Bash(CLAUDE_CODE_GIT_BASH_PATH=\"D:/Program Files/Git/bin/bash.exe\" claude mcp:*)",
-      "Bash(CLAUDE_CODE_GIT_BASH_PATH='D:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe' claude mcp:*)"
+      "Bash(CLAUDE_CODE_GIT_BASH_PATH='D:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe' claude mcp:*)",
+      "Bash(git:*)",
+      "Bash(python:*)",
+      "Bash(npm:*)"
     ]
   }
 }
diff --git a/rsf-design/.env.development b/rsf-design/.env.development
index 8b3e566..3e69e34 100644
--- a/rsf-design/.env.development
+++ b/rsf-design/.env.development
@@ -13,4 +13,4 @@
 VITE_DROP_CONSOLE = false
 
 # 鏄惁寮�鍚� Vue DevTools / Inspector锛堝紑鍚悗浼氬鍔犳湰鍦版ā鍧楄浆鎹㈣�楁椂锛�
-VITE_ENABLE_VUE_DEVTOOLS = false
+VITE_ENABLE_VUE_DEVTOOLS = true
diff --git a/rsf-design/src/api/wave.js b/rsf-design/src/api/wave.js
index 4695632..84b69ff 100644
--- a/rsf-design/src/api/wave.js
+++ b/rsf-design/src/api/wave.js
@@ -103,6 +103,13 @@
   })
 }
 
+export function fetchCreateOutStockWave(payload = {}) {
+  return request.post({
+    url: '/outStock/generate/wave',
+    data: payload
+  })
+}
+
 export async function fetchExportWaveReport(payload = {}, options = {}) {
   return fetch(`${import.meta.env.VITE_API_URL}/wave/export`, {
     method: 'POST',
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
index cc520d7..7b7b4ba 100644
--- a/rsf-design/src/locales/langs/zh.json
+++ b/rsf-design/src/locales/langs/zh.json
@@ -1415,6 +1415,7 @@
           "view": "鏌ョ湅璇︽儏",
           "items": "鏄庣粏",
           "print": "鎵撳嵃",
+          "createWave": "鐢熸垚娉㈡",
           "complete": "瀹屾垚",
           "cancel": "鍙栨秷",
           "delete": "鍒犻櫎"
@@ -1466,6 +1467,11 @@
           "detailTimeout": "鍑哄簱鍗曡鎯呭姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�",
           "itemsTimeout": "鍑哄簱鍗曟槑缁嗗姞杞借秴鏃讹紝宸插仠姝㈢瓑寰�",
           "detailLoadFailed": "鑾峰彇鍑哄簱鍗曡鎯呭け璐�",
+          "createWaveTitle": "鐢熸垚娉㈡",
+          "createWaveConfirm": "纭畾涓哄凡閫夋嫨鐨� {count} 鏉″嚭搴撳崟鐢熸垚娉㈡鍚楋紵",
+          "createWaveSuccess": "娉㈡鐢熸垚鎴愬姛",
+          "createWaveFailed": "娉㈡鐢熸垚澶辫触",
+          "createWaveSelectionRequired": "璇峰厛閫夋嫨鍑哄簱鍗�",
           "completeTitle": "瀹屾垚纭",
           "completeConfirm": "纭畾瀹屾垚鍑哄簱鍗� {code} 鍚楋紵",
           "completeSuccess": "瀹屾垚鎴愬姛",
diff --git a/rsf-design/src/views/orders/out-stock/index.vue b/rsf-design/src/views/orders/out-stock/index.vue
index c3d5018..77e0798 100644
--- a/rsf-design/src/views/orders/out-stock/index.vue
+++ b/rsf-design/src/views/orders/out-stock/index.vue
@@ -11,20 +11,25 @@
     <ElCard class="art-table-card">
       <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
         <template #left>
-          <ListExportPrint
-            :preview-visible="previewVisible"
-            @update:previewVisible="handlePreviewVisibleChange"
-            :report-title="reportTitle"
-            :selected-rows="selectedRows"
-            :query-params="reportQueryParams"
-            :columns="columns"
-            :preview-rows="previewRows"
-            :preview-meta="resolvedPreviewMeta"
-            :total="pagination.total"
-            :disabled="loading"
-            @export="handleExport"
-            @print="handlePrint"
-          />
+          <ElSpace wrap>
+            <ElButton type="primary" :loading="createWaveLoading" :disabled="loading || selectedRows.length === 0" @click="handleCreateWave">
+              {{ t('pages.orders.outStock.actions.createWave') }}
+            </ElButton>
+            <ListExportPrint
+              :preview-visible="previewVisible"
+              @update:previewVisible="handlePreviewVisibleChange"
+              :report-title="reportTitle"
+              :selected-rows="selectedRows"
+              :query-params="reportQueryParams"
+              :columns="columns"
+              :preview-rows="previewRows"
+              :preview-meta="resolvedPreviewMeta"
+              :total="pagination.total"
+              :disabled="loading"
+              @export="handleExport"
+              @print="handlePrint"
+            />
+          </ElSpace>
         </template>
       </ArtTableHeader>
 
@@ -56,7 +61,7 @@
 
 <script setup>
   import { computed, reactive, ref } from 'vue'
-  import { ElMessage, ElMessageBox } from 'element-plus'
+  import { ElButton, ElMessage, ElMessageBox, ElSpace } from 'element-plus'
   import { useRouter } from 'vue-router'
   import { useI18n } from 'vue-i18n'
   import { useUserStore } from '@/store/modules/user'
@@ -75,14 +80,17 @@
     fetchGetOutStockMany,
     fetchOutStockPage
   } from '@/api/out-stock'
+  import { fetchCreateOutStockWave } from '@/api/wave'
   import OutStockDetailDrawer from './modules/out-stock-detail-drawer.vue'
   import {
     OUT_STOCK_REPORT_STYLE,
+    buildCreateWavePayload,
     buildOutStockPageQueryParams,
     buildOutStockPrintRows,
     buildOutStockReportMeta,
     buildOutStockSearchParams,
     createOutStockSearchState,
+    getCreateWaveValidationMessage,
     getOutStockReportTitle,
     normalizeOutStockRow
   } from './outStockPage.helpers'
@@ -104,6 +112,7 @@
   const detailData = ref({})
   const detailItemRows = ref([])
   const activeOutStockId = ref(null)
+  const createWaveLoading = ref(false)
   const detailItemPagination = reactive({
     current: 1,
     size: 20,
@@ -269,6 +278,40 @@
     loadDetailResources()
   }
 
+  async function handleCreateWave() {
+    const validationMessage = getCreateWaveValidationMessage(selectedRows.value, t)
+    if (validationMessage) {
+      ElMessage.warning(validationMessage)
+      return
+    }
+
+    try {
+      await ElMessageBox.confirm(
+        t('pages.orders.outStock.messages.createWaveConfirm', {
+          count: selectedRows.value.length
+        }),
+        t('pages.orders.outStock.messages.createWaveTitle'),
+        {
+          confirmButtonText: t('common.confirm'),
+          cancelButtonText: t('common.cancel'),
+          type: 'warning'
+        }
+      )
+
+      createWaveLoading.value = true
+      await fetchCreateOutStockWave(buildCreateWavePayload(selectedRows.value))
+      ElMessage.success(t('pages.orders.outStock.messages.createWaveSuccess'))
+      selectedRows.value = []
+      await refreshData()
+      router.push('/orders/wave')
+    } catch (error) {
+      if (error === 'cancel' || error?.message === 'cancel') return
+      ElMessage.error(error?.message || t('pages.orders.outStock.messages.createWaveFailed'))
+    } finally {
+      createWaveLoading.value = false
+    }
+  }
+
   async function handleComplete(row) {
     try {
       await ElMessageBox.confirm(t('pages.orders.outStock.messages.completeConfirm', { code: row.code || '' }), t('pages.orders.outStock.messages.completeTitle'), {
diff --git a/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js b/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
index 7e213da..b35e20d 100644
--- a/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
+++ b/rsf-design/src/views/orders/out-stock/outStockPage.helpers.js
@@ -35,6 +35,14 @@
   return String(value ?? '').trim()
 }
 
+function normalizeIds(rows = []) {
+  return Array.isArray(rows)
+    ? rows
+        .map((row) => Number(row?.id))
+        .filter((id) => Number.isFinite(id))
+    : []
+}
+
 function normalizeNumber(value, fallback = void 0) {
   if (value === '' || value === null || value === undefined) {
     return fallback
@@ -196,6 +204,20 @@
   }
 }
 
+export function buildCreateWavePayload(rows = []) {
+  return {
+    ids: normalizeIds(rows)
+  }
+}
+
+export function getCreateWaveValidationMessage(rows = [], t) {
+  const ids = normalizeIds(rows)
+  if (ids.length === 0) {
+    return translate(t, 'pages.orders.outStock.messages.createWaveSelectionRequired')
+  }
+  return ''
+}
+
 export function getOutStockActionList(row = {}, t) {
   const normalizedRow = normalizeOutStockRow(row, t)
   return [
diff --git a/rsf-open-api/src/main/resources/application-dev.yml b/rsf-open-api/src/main/resources/application-dev.yml
index ad49eca..6536f74 100644
--- a/rsf-open-api/src/main/resources/application-dev.yml
+++ b/rsf-open-api/src/main/resources/application-dev.yml
@@ -27,23 +27,24 @@
       min-idle: 5
       max-active: 20
       max-wait: 30000
+      keep-alive: true
       time-between-eviction-runs-millis: 60000
       min-evictable-idle-time-millis: 300000
       test-while-idle: true
-      test-on-borrow: true
+      test-on-borrow: false
       test-on-return: false
       remove-abandoned: true
       remove-abandoned-timeout: 1800
       #pool-prepared-statements: false
       #max-pool-prepared-statement-per-connection-size: 20
       filters: stat, wall
-      validation-query: SELECT 'x'
-      aop-patterns: com.zy.*.*.service.*
+      validation-query: SELECT 1
+      aop-patterns: com.vincent.rsf.openApi.*.service.*
       stat-view-servlet:
         url-pattern: /druid/*
         reset-enable: true
-        login-username: admin
-        login-password: admin
+        login-username: ${DRUID_STAT_USER:admin}
+        login-password: ${DRUID_STAT_PWD:admin123}
         enabled: true
   servlet:
     multipart:
diff --git a/rsf-open-api/src/main/resources/application-prod.yml b/rsf-open-api/src/main/resources/application-prod.yml
index 9c850fa..9501c2b 100644
--- a/rsf-open-api/src/main/resources/application-prod.yml
+++ b/rsf-open-api/src/main/resources/application-prod.yml
@@ -19,24 +19,25 @@
       min-idle: 5
       max-active: 20
       max-wait: 30000
+      keep-alive: true
       time-between-eviction-runs-millis: 60000
       min-evictable-idle-time-millis: 300000
       test-while-idle: true
-      test-on-borrow: true
+      test-on-borrow: false
       test-on-return: false
-      remove-abandoned: true
+      remove-abandoned: false
       remove-abandoned-timeout: 1800
       #pool-prepared-statements: false
       #max-pool-prepared-statement-per-connection-size: 20
       filters: stat, wall
-      validation-query: SELECT 'x'
-      aop-patterns: com.zy.*.*.service.*
+      validation-query: SELECT 1
+      aop-patterns: com.vincent.rsf.openApi.*.service.*
       stat-view-servlet:
         url-pattern: /druid/*
         reset-enable: true
-        login-username: admin
-        login-password: admin
-        enabled: true
+        login-username: ${DRUID_STAT_USER:admin}
+        login-password: ${DRUID_STAT_PWD:admin123}
+        enabled: false
   servlet:
     multipart:
       maxFileSize: 100MB
@@ -84,4 +85,4 @@
       #閾炬帴
       host: http://www.itsdg.cn
       #绔彛
-      port: 3741
\ No newline at end of file
+      port: 3741
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/common/exception/GlobalExceptionHandler.java b/rsf-server/src/main/java/com/vincent/rsf/server/common/exception/GlobalExceptionHandler.java
index 558bec6..ea691e6 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/common/exception/GlobalExceptionHandler.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/common/exception/GlobalExceptionHandler.java
@@ -12,6 +12,7 @@
 import org.springframework.web.bind.annotation.ControllerAdvice;
 import org.springframework.web.bind.annotation.ExceptionHandler;
 import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
 
 import jakarta.servlet.http.HttpServletResponse;
 import java.util.regex.Matcher;
@@ -79,10 +80,18 @@
         return R.error(out);
     }
 
+    @ExceptionHandler(AsyncRequestNotUsableException.class)
+    public void asyncRequestNotUsableExceptionHandler(AsyncRequestNotUsableException e) {
+        logger.warn("Client connection aborted: {}", resolveAbortMessage(e));
+    }
 
     @ResponseBody
     @ExceptionHandler(RuntimeException.class)
     public R runtimeExceptionHandler(RuntimeException e, HttpServletResponse response) {
+        if (isClientAbortException(e)) {
+            logger.warn("Client connection aborted: {}", resolveAbortMessage(e));
+            return null;
+        }
         CommonUtil.addCrossHeaders(response);
         Throwable cause = e.getCause();
         if (cause instanceof CoolException) {
@@ -95,10 +104,44 @@
     @ResponseBody
     @ExceptionHandler(Throwable.class)
     public R exceptionHandler(Throwable e, HttpServletResponse response) {
+        if (isClientAbortException(e)) {
+            logger.warn("Client connection aborted: {}", resolveAbortMessage(e));
+            return null;
+        }
         logger.error(e.getMessage(), e);
         CommonUtil.addCrossHeaders(response);
         return R.error(Constants.RESULT_ERROR_MSG);
     }
 
+    private boolean isClientAbortException(Throwable throwable) {
+        Throwable current = throwable;
+        while (current != null) {
+            String message = current.getMessage();
+            if (message != null) {
+                String normalized = message.toLowerCase();
+                if (normalized.contains("broken pipe")
+                        || normalized.contains("connection reset")
+                        || normalized.contains("forcibly closed")
+                        || normalized.contains("abort")) {
+                    return true;
+                }
+            }
+            current = current.getCause();
+        }
+        return false;
+    }
+
+    private String resolveAbortMessage(Throwable throwable) {
+        Throwable current = throwable;
+        while (current != null) {
+            String message = current.getMessage();
+            if (message != null && !message.isBlank()) {
+                return message;
+            }
+            current = current.getCause();
+        }
+        return "client aborted connection";
+    }
+
 }
 
diff --git a/rsf-server/src/main/resources/application-dev.yml b/rsf-server/src/main/resources/application-dev.yml
index 5e7940f..ce088af 100644
--- a/rsf-server/src/main/resources/application-dev.yml
+++ b/rsf-server/src/main/resources/application-dev.yml
@@ -25,10 +25,11 @@
       min-idle: 5
       max-active: 20
       max-wait: 30000
+      keep-alive: true
       time-between-eviction-runs-millis: 60000
       min-evictable-idle-time-millis: 300000
       test-while-idle: true
-      test-on-borrow: true
+      test-on-borrow: false
       test-on-return: false
       remove-abandoned: true
       remove-abandoned-timeout: 1800
@@ -36,12 +37,12 @@
       #max-pool-prepared-statement-per-connection-size: 20
       filters: stat, wall
       validation-query: SELECT 1
-      aop-patterns: com.zy.*.*.service.*
+      aop-patterns: com.vincent.rsf.server.*.service.*
       stat-view-servlet:
         url-pattern: /druid/*
         reset-enable: true
-        login-username: admin
-        login-password: admin
+        login-username: ${DRUID_STAT_USER:admin}
+        login-password: ${DRUID_STAT_PWD:admin123}
         enabled: true
   servlet:
     multipart:
diff --git a/rsf-server/src/main/resources/application-prod.yml b/rsf-server/src/main/resources/application-prod.yml
index 569d601..fc96c6b 100644
--- a/rsf-server/src/main/resources/application-prod.yml
+++ b/rsf-server/src/main/resources/application-prod.yml
@@ -19,24 +19,25 @@
       min-idle: 5
       max-active: 20
       max-wait: 30000
+      keep-alive: true
       time-between-eviction-runs-millis: 60000
       min-evictable-idle-time-millis: 300000
       test-while-idle: true
-      test-on-borrow: true
+      test-on-borrow: false
       test-on-return: false
-      remove-abandoned: true
+      remove-abandoned: false
       remove-abandoned-timeout: 1800
       #pool-prepared-statements: false
       #max-pool-prepared-statement-per-connection-size: 20
       filters: stat, wall
-      validation-query: SELECT 'x'
-      aop-patterns: com.zy.*.*.service.*
+      validation-query: SELECT 1
+      aop-patterns: com.vincent.rsf.server.*.service.*
       stat-view-servlet:
         url-pattern: /druid/*
         reset-enable: true
-        login-username: admin
-        login-password: admin
-        enabled: true
+        login-username: ${DRUID_STAT_USER:admin}
+        login-password: ${DRUID_STAT_PWD:admin123}
+        enabled: false
   servlet:
     multipart:
       maxFileSize: 100MB

--
Gitblit v1.9.1