#
zhou zhou
2 天以前 523365960513f297024a419f94b2b42eccd9456f
#
2个文件已添加
15个文件已修改
1231 ■■■■■ 已修改文件
rsf-design/package.json 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/pnpm-lock.yaml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/scripts/build-local-iconify-collections.mjs 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/asn-order-log.js 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/plugins/iconify.collections.js 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/dashboard/console/index.vue 279 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/orders/asn-order-log/index.vue 200 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/CursorPageParam.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/CursorPageResult.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/AsnOrderLogController.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/AsnOrderLog.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/AsnOrderMapper.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/AsnOrderPressureSchedules.java 131 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderServiceImpl.java 140 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/BaseController.java 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-server/src/main/resources/mapper/manager/AsnOrderMapper.xml 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/package.json
@@ -26,6 +26,7 @@
    "@iconify-json/ix": "^1.2.11",
    "@iconify-json/line-md": "^1.2.16",
    "@iconify-json/ri": "^1.2.10",
    "@iconify-json/solar": "^1.2.5",
    "@iconify-json/svg-spinners": "^1.2.4",
    "@iconify-json/system-uicons": "^1.2.4",
    "@iconify-json/vaadin": "^1.2.1",
rsf-design/pnpm-lock.yaml
@@ -29,6 +29,9 @@
      '@iconify-json/ri':
        specifier: ^1.2.10
        version: 1.2.10
      '@iconify-json/solar':
        specifier: ^1.2.5
        version: 1.2.5
      '@iconify-json/svg-spinners':
        specifier: ^1.2.4
        version: 1.2.4
@@ -628,6 +631,9 @@
  '@iconify-json/ri@1.2.10':
    resolution: {integrity: sha512-WWMhoncVVM+Xmu9T5fgu2lhYRrKTEWhKk3Com0KiM111EeEsRLiASjpsFKnC/SrB6covhUp95r2mH8tGxhgd5Q==}
  '@iconify-json/solar@1.2.5':
    resolution: {integrity: sha512-WMAiNwchU8zhfrySww6KQBRIBbsQ6SvgIu2yA+CHGyMima/0KQwT5MXogrZPJGoQF+1Ye3Qj6K+1CiyNn3YkoA==}
  '@iconify-json/svg-spinners@1.2.4':
    resolution: {integrity: sha512-ayn0pogFPwJA1WFZpDnoq9/hjDxN+keeCMyThaX4d3gSJ3y0mdKUxIA/b1YXWGtY9wVtZmxwcvOIeEieG4+JNg==}
@@ -3537,6 +3543,10 @@
    dependencies:
      '@iconify/types': 2.0.0
  '@iconify-json/solar@1.2.5':
    dependencies:
      '@iconify/types': 2.0.0
  '@iconify-json/svg-spinners@1.2.4':
    dependencies:
      '@iconify/types': 2.0.0
rsf-design/scripts/build-local-iconify-collections.mjs
@@ -9,6 +9,7 @@
import { icons as ixIcons } from '@iconify-json/ix'
import { icons as lineMdIcons } from '@iconify-json/line-md'
import { icons as remixIcons } from '@iconify-json/ri'
import { icons as solarIcons } from '@iconify-json/solar'
import { icons as svgSpinnersIcons } from '@iconify-json/svg-spinners'
import { icons as systemUiconsIcons } from '@iconify-json/system-uicons'
import { icons as vaadinIcons } from '@iconify-json/vaadin'
@@ -25,6 +26,7 @@
  ix: ixIcons,
  'line-md': lineMdIcons,
  ri: remixIcons,
  solar: solarIcons,
  'svg-spinners': svgSpinnersIcons,
  'system-uicons': systemUiconsIcons,
  vaadin: vaadinIcons
rsf-design/src/api/asn-order-log.js
@@ -1,4 +1,5 @@
import request from '@/utils/http'
import { buildAsnOrderLogPageQueryParams } from '@/views/orders/asn-order-log/asnOrderLogPage.helpers'
function normalizeText(value) {
  return typeof value === 'string' ? value.trim() : value
@@ -31,11 +32,7 @@
}
export function buildAsnOrderLogPageParams(params = {}) {
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    ...filterParams(params, ['current', 'pageSize', 'size'])
  }
  return buildAsnOrderLogPageQueryParams(params)
}
export function buildAsnOrderItemLogPageParams(params = {}) {
rsf-design/src/plugins/iconify.collections.js
@@ -112,8 +112,11 @@
      'book-2-line': {
        body: '<path fill="currentColor" d="M21 18H6a1 1 0 1 0 0 2h15v2H6a3 3 0 0 1-3-3V4a2 2 0 0 1 2-2h16zM5 16.05q.243-.05.5-.05H19V4H5zM16 9H8V7h8z"/>'
      },
      'chat-1-line': {
        body: '<path fill="currentColor" d="M10 3h4a8 8 0 1 1 0 16v3.5c-5-2-12-5-12-11.5a8 8 0 0 1 8-8m2 14h2a6 6 0 0 0 0-12h-4a6 6 0 0 0-6 6c0 3.61 2.462 5.966 8 8.48z"/>'
      'chat-3-line': {
        body: '<path fill="currentColor" d="M7.291 20.824L2 22l1.176-5.291A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.709-1.176m.29-2.113l.653.35A7.96 7.96 0 0 0 12 20a8 8 0 1 0-8-8c0 1.335.325 2.617.94 3.766l.349.653l-.655 2.947z"/>'
      },
      'chat-history-line': {
        body: '<path fill="currentColor" d="M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10a9.96 9.96 0 0 1-4.708-1.175L2 22l1.176-5.29A9.96 9.96 0 0 1 2 12C2 6.477 6.477 2 12 2m0 2a8 8 0 0 0-8 8c0 1.335.326 2.618.94 3.766l.35.654l-.656 2.946l2.948-.654l.653.349A7.96 7.96 0 0 0 12 20a8 8 0 1 0 0-16m1 3v5h4v2h-6V7z"/>'
      },
      'check-fill': {
        body: '<path fill="currentColor" d="m10 15.17l9.192-9.191l1.414 1.414L10 17.999l-6.364-6.364l1.414-1.414z"/>'
@@ -136,6 +139,9 @@
      'close-large-fill': {
        body: '<path fill="currentColor" d="M10.586 12L2.793 4.207l1.414-1.414L12 10.586l7.793-7.793l1.414 1.414L13.414 12l7.793 7.793l-1.414 1.414L12 13.414l-7.793 7.793l-1.414-1.414z"/>'
      },
      'close-line': {
        body: '<path fill="currentColor" d="m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z"/>'
      },
      'coin-line': {
        body: '<path fill="currentColor" d="M12.005 4.003c6.075 0 11 2.686 11 6v4c0 3.314-4.925 6-11 6c-5.967 0-10.824-2.591-10.995-5.823l-.005-.177v-4c0-3.314 4.925-6 11-6m0 12c-3.72 0-7.01-1.008-9-2.55v.55c0 1.882 3.883 4 9 4c5.01 0 8.838-2.03 8.995-3.882l.005-.118l.001-.55c-1.99 1.542-5.28 2.55-9.001 2.55m0-10c-5.117 0-9 2.118-9 4s3.883 4 9 4s9-2.118 9-4s-3.883-4-9-4"/>'
      },
@@ -145,6 +151,9 @@
      'computer-line': {
        body: '<path fill="currentColor" d="M4 16h16V5H4zm9 2v2h4v2H7v-2h4v-2H2.992A1 1 0 0 1 2 16.992V4.008C2 3.451 2.455 3 2.992 3h18.016c.548 0 .992.449.992 1.007v12.985c0 .557-.455 1.008-.992 1.008z"/>'
      },
      'cpu-line': {
        body: '<path fill="currentColor" d="M6 18h12V6H6zm8 2h-4v2H8v-2H5a1 1 0 0 1-1-1v-3H2v-2h2v-4H2V8h2V5a1 1 0 0 1 1-1h3V2h2v2h4V2h2v2h3a1 1 0 0 1 1 1v3h2v2h-2v4h2v2h-2v3a1 1 0 0 1-1 1h-3v2h-2zM8 8h8v8H8z"/>'
      },
      'delete-bin-4-line': {
        body: '<path fill="currentColor" d="M20 7v14a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7H2V5h20v2zM6 7v13h12V7zm1-5h10v2H7zm4 8h2v7h-2z"/>'
      },
@@ -153,6 +162,9 @@
      },
      'delete-bin-6-line': {
        body: '<path fill="currentColor" d="M7 4V2h10v2h5v2h-2v15a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V6H2V4zM6 6v14h12V6zm3 3h2v8H9zm4 0h2v8h-2z"/>'
      },
      'delete-bin-line': {
        body: '<path fill="currentColor" d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1zm1 2H6v12h12zm-9 3h2v6H9zm4 0h2v6h-2zM9 4v2h6V4z"/>'
      },
      'drag-move-2-fill': {
        body: '<path fill="currentColor" d="M18 11V8l4 4l-4 4v-3h-5v5h3l-4 4l-4-4h3v-5H6v3l-4-4l4-4v3h5V6H8l4-4l4 4h-3v5z"/>'
@@ -165,6 +177,9 @@
      },
      'edit-2-line': {
        body: '<path fill="currentColor" d="M5 18.89h1.414l9.314-9.314l-1.414-1.414L5 17.476zm16 2H3v-4.243L16.435 3.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L9.243 18.89H21zM15.728 6.748l1.414 1.414l1.414-1.414l-1.414-1.414z"/>'
      },
      'edit-line': {
        body: '<path fill="currentColor" d="M6.414 15.89L16.556 5.748l-1.414-1.414L5 14.476v1.414zm.829 2H3v-4.243L14.435 2.212a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414zM3 19.89h18v2H3z"/>'
      },
      'error-warning-line': {
        body: '<path fill="currentColor" d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10s-4.477 10-10 10m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16m-1-5h2v2h-2zm0-8h2v6h-2z"/>'
@@ -193,12 +208,6 @@
      'fullscreen-line': {
        body: '<path fill="currentColor" d="M8 3v2H4v4H2V3zM2 21v-6h2v4h4v2zm20 0h-6v-2h4v-4h2zm0-12h-2V5h-4V3h6z"/>'
      },
      'function-line': {
        body: '<path fill="currentColor" d="M3 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1zM13 4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm0 10a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v6a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1zm2-9v4h4V5zm0 10v4h4v-4zM5 5v4h4V5zm0 10v4h4v-4z"/>'
      },
      'github-line': {
        body: '<path fill="currentColor" d="M5.884 18.653c-.3-.2-.558-.455-.86-.816a51 51 0 0 1-.466-.579c-.463-.575-.755-.841-1.056-.95a1 1 0 1 1 .675-1.882c.752.27 1.261.735 1.947 1.588c-.094-.117.34.427.433.539c.19.227.33.365.44.438c.204.137.588.196 1.15.14c.024-.382.094-.753.202-1.095c-2.968-.726-4.648-2.64-4.648-6.396c0-1.24.37-2.356 1.058-3.292c-.218-.894-.185-1.975.302-3.192a1 1 0 0 1 .63-.582c.081-.024.127-.035.208-.047c.803-.124 1.937.17 3.415 1.096a11.7 11.7 0 0 1 2.687-.308c.912 0 1.819.104 2.684.308c1.477-.933 2.614-1.227 3.422-1.096q.128.02.218.05a1 1 0 0 1 .616.58c.487 1.216.52 2.296.302 3.19c.691.936 1.058 2.045 1.058 3.293c0 3.757-1.674 5.665-4.642 6.392c.125.415.19.878.19 1.38c0 .665-.002 1.299-.007 2.01c0 .19-.002.394-.005.706a1 1 0 0 1-.018 1.958c-1.14.227-1.984-.532-1.984-1.525l.002-.447l.005-.705c.005-.707.008-1.337.008-1.997c0-.697-.184-1.152-.426-1.361c-.661-.57-.326-1.654.541-1.751c2.966-.333 4.336-1.482 4.336-4.66c0-.955-.312-1.744-.913-2.404A1 1 0 0 1 17.2 6.19c.166-.414.236-.957.095-1.614l-.01.003c-.491.139-1.11.44-1.858.949a1 1 0 0 1-.833.135a9.6 9.6 0 0 0-2.592-.349c-.89 0-1.772.118-2.592.35a1 1 0 0 1-.829-.134c-.753-.507-1.374-.807-1.87-.947c-.143.653-.072 1.194.093 1.607a1 1 0 0 1-.189 1.045c-.597.655-.913 1.458-.913 2.404c0 3.172 1.371 4.328 4.322 4.66c.865.097 1.202 1.177.545 1.748c-.193.168-.43.732-.43 1.364v3.15c0 .985-.834 1.725-1.96 1.528a1 1 0 0 1-.04-1.962v-.99c-.91.061-1.661-.088-2.254-.485"/>'
      },
      'group-line': {
        body: '<path fill="currentColor" d="M2 22a8 8 0 1 1 16 0h-2a6 6 0 0 0-12 0zm8-9c-3.315 0-6-2.685-6-6s2.685-6 6-6s6 2.685 6 6s-2.685 6-6 6m0-2c2.21 0 4-1.79 4-4s-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4m8.284 3.703A8 8 0 0 1 23 22h-2a6 6 0 0 0-3.537-5.473zm-.688-11.29A5.5 5.5 0 0 1 21 8.5a5.5 5.5 0 0 1-5 5.478v-2.013a3.5 3.5 0 0 0 1.041-6.609z"/>'
      },
@@ -226,8 +235,14 @@
      'line-chart-line': {
        body: '<path fill="currentColor" d="M5 3v16h16v2H3V3zm15.293 3.293l1.414 1.414L16 13.414l-3-2.999l-4.293 4.292l-1.414-1.414L13 7.586l3 2.999z"/>'
      },
      'list-check-3': {
        body: '<path fill="currentColor" d="M8 6v3H5V6zM3 4v7h7V4zm10 0h8v2h-8zm0 7h8v2h-8zm0 7h8v2h-8zm-2.293-1.793l-1.414-1.414L6 18.086l-1.793-1.793l-1.414 1.414L6 20.914z"/>'
      },
      'lock-line': {
        body: '<path fill="currentColor" d="M19 10h1a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V11a1 1 0 0 1 1-1h1V9a7 7 0 0 1 14 0zM5 12v8h14v-8zm6 2h2v4h-2zm6-4V9A5 5 0 0 0 7 9v1z"/>'
      },
      'magic-line': {
        body: '<path fill="currentColor" d="M15.199 9.944a2.6 2.6 0 0 1-.79-1.55l-.403-3.083l-2.731 1.486a2.6 2.6 0 0 1-1.719.272L6.5 6.5l.57 3.056a2.6 2.6 0 0 1-.273 1.72l-1.486 2.73l3.083.403a2.6 2.6 0 0 1 1.55.79l2.138 2.257l1.336-2.807a2.6 2.6 0 0 1 1.23-1.231l2.808-1.336zm.025 5.564l-2.213 4.65a.6.6 0 0 1-.977.155l-3.542-3.739a.6.6 0 0 0-.358-.182l-5.106-.668a.6.6 0 0 1-.45-.881l2.462-4.524a.6.6 0 0 0 .063-.396L4.16 4.86a.6.6 0 0 1 .7-.7l5.062.943a.6.6 0 0 0 .397-.063l4.523-2.46a.6.6 0 0 1 .882.448l.668 5.107a.6.6 0 0 0 .182.357l3.739 3.542a.6.6 0 0 1-.155.977l-4.65 2.213a.6.6 0 0 0-.284.284m.797 1.927l1.414-1.414l4.243 4.242l-1.415 1.415z"/>'
      },
      'mail-line': {
        body: '<path fill="currentColor" d="M3 3h18a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1m17 4.238l-7.928 7.1L4 7.216V19h16zM4.511 5l7.55 6.662L19.502 5z"/>'
@@ -253,8 +268,8 @@
      'more-2-fill': {
        body: '<path fill="currentColor" d="M12 3c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0 14c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2m0-7c-1.1 0-2 .9-2 2s.9 2 2 2s2-.9 2-2s-.9-2-2-2"/>'
      },
      'notification-2-line': {
        body: '<path fill="currentColor" d="M22 20H2v-2h1v-6.969C3 6.043 7.03 2 12 2s9 4.043 9 9.031V18h1zM5 18h14v-6.969C19 7.148 15.866 4 12 4s-7 3.148-7 7.031zm4.5 3h5a2.5 2.5 0 0 1-5 0"/>'
      'node-tree': {
        body: '<path fill="currentColor" d="M10 2a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H8v2h5V9a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-1H8v6h5v-1a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-6a1 1 0 0 1-1-1v-1H7a1 1 0 0 1-1-1V8H4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1zm9 16h-4v2h4zm0-8h-4v2h4zM9 4H5v2h4z"/>'
      },
      'notification-3-line': {
        body: '<path fill="currentColor" d="M20 17h2v2H2v-2h2v-7a8 8 0 1 1 16 0zm-2 0v-7a6 6 0 0 0-12 0v7zm-9 4h6v2H9z"/>'
@@ -319,6 +334,9 @@
      'search-line': {
        body: '<path fill="currentColor" d="m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z"/>'
      },
      'send-plane-2-line': {
        body: '<path fill="currentColor" d="M3.5 1.346a.5.5 0 0 1 .241.061l18.462 10.155a.5.5 0 0 1 0 .876L3.741 22.592A.5.5 0 0 1 3 22.154V1.846a.5.5 0 0 1 .5-.5M5 4.382V11h5v2H5v6.617L18.85 12z"/>'
      },
      'send-plane-line': {
        body: '<path fill="currentColor" d="m21.727 2.957l-5.454 19.086c-.15.529-.475.553-.717.07L11 13L1.923 9.37c-.51-.205-.503-.51.034-.689L21.043 2.32c.529-.176.832.12.684.638m-2.692 2.14L6.812 9.17l5.637 2.255l3.04 6.08z"/>'
      },
@@ -333,6 +351,9 @@
      },
      'shield-check-line': {
        body: '<path fill="currentColor" d="m12 1l8.217 1.826a1 1 0 0 1 .783.976v9.987a6 6 0 0 1-2.672 4.992L12 23l-6.328-4.219A6 6 0 0 1 3 13.79V3.802a1 1 0 0 1 .783-.976zm0 2.049L5 4.604v9.185a4 4 0 0 0 1.781 3.328L12 20.597l5.219-3.48A4 4 0 0 0 19 13.79V4.604zm4.452 5.173l1.415 1.414L11.503 16L7.26 11.757l1.414-1.414l2.828 2.828z"/>'
      },
      'sidebar-unfold-line': {
        body: '<path fill="currentColor" d="M5 5h8v14H5zm14 14h-4V5h4zM4 3a1 1 0 0 0-1 1v16a1 1 0 0 0 1 1h16a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm7 9L7 8.5v7z"/>'
      },
      'smartphone-line': {
        body: '<path fill="currentColor" d="M7 4v16h10V4zM6 2h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1m6 15a1 1 0 1 1 0 2a1 1 0 0 1 0-2"/>'
@@ -389,6 +410,16 @@
    prefix: 'ri',
    width: 24
  },
  solar: {
    height: 24,
    icons: {
      'double-alt-arrow-right-linear': {
        body: '<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"><path d="m11 19l6-7l-6-7"/><path d="m7 19l6-7l-6-7"/></g>'
      }
    },
    prefix: 'solar',
    width: 24
  },
  vaadin: {
    icons: {
      'ctrl-a': {
rsf-design/src/views/dashboard/console/index.vue
@@ -1,143 +1,114 @@
<template>
  <div class="art-full-height flex flex-col gap-6">
    <section class="grid gap-6 md:grid-cols-2 xl:grid-cols-4" v-loading="sectionLoading.summary">
  <div class="art-full-height flex flex-col gap-6 lg:min-h-0 lg:overflow-hidden lg:gap-2">
    <section class="grid gap-3 md:grid-cols-2 lg:grid-cols-4 lg:shrink-0" v-loading="sectionLoading.summary">
      <div
        v-for="item in summaryCardItems"
        :key="item.title"
        class="art-card flex items-start justify-between rounded-3xl px-7 py-6"
        class="art-card flex items-start justify-between rounded-3xl px-4 py-4 lg:px-4 lg:py-3 xl:px-5"
      >
        <div class="min-w-0 pr-6">
          <p class="text-sm font-medium text-g-700">{{ item.title }}</p>
          <ArtCountTo class="mt-3 block text-[2.3rem] font-semibold leading-none text-g-900" :target="item.count" :duration="1400" />
          <div class="mt-4 flex items-center gap-2 text-sm">
            <span :class="item.metaTone">{{ item.metaLabel }}</span>
            <span class="text-g-500">{{ item.metaValue }}</span>
        <div class="min-w-0 flex-1 pr-3">
          <p class="truncate text-sm font-medium text-g-700 lg:text-[13px]">{{ item.title }}</p>
          <ArtCountTo
            class="mt-1 block truncate text-[1.95rem] font-semibold leading-none text-g-900 lg:text-[1.72rem] xl:text-[1.92rem]"
            :target="item.count"
            :duration="1400"
          />
          <div class="mt-1.5 flex items-center gap-1 text-xs lg:text-[12px] xl:text-sm">
            <span :class="item.metaTone" class="shrink-0">{{ item.metaLabel }}</span>
            <span class="truncate text-g-500">{{ item.metaValue }}</span>
          </div>
        </div>
        <div class="flex size-13 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass">
          <ArtSvgIcon :icon="item.icon" class="text-2xl" :class="item.iconClass" />
        <div class="flex size-10 shrink-0 items-center justify-center rounded-2xl lg:size-9 xl:size-11" :class="item.iconBoxClass">
          <ArtSvgIcon :icon="item.icon" class="text-xl xl:text-2xl" :class="item.iconClass" />
        </div>
      </div>
    </section>
    <section class="grid gap-6 xl:grid-cols-[1.35fr_1fr]">
      <div class="art-card h-115 overflow-hidden p-6 box-border">
        <div class="art-card-header">
          <div class="title">
            <h4>近 30 天出入库趋势</h4>
            <p>真实链路 <span class="text-success">已接通</span></p>
    <section class="flex min-h-0 flex-1 flex-col gap-2">
      <div class="grid min-h-0 flex-1 gap-2 lg:grid-cols-[1.35fr_1fr]">
        <div class="art-card box-border flex h-full min-h-0 flex-col overflow-hidden p-4 lg:p-5">
          <div class="art-card-header">
            <div class="title">
              <h4>近 30 天出入库趋势</h4>
              <p>真实链路 <span class="text-success">已接通</span></p>
            </div>
          </div>
          <div class="mt-3 min-h-0 flex-1">
            <ArtBarChart
              height="100%"
              :loading="sectionLoading.trend"
              :data="trendChartSeries"
              :x-axis-data="trendChartXAxisData"
              :show-axis-line="false"
              :show-legend="true"
              :show-split-line="true"
              legend-position="top"
              bar-width="38%"
            />
          </div>
        </div>
        <div class="h-[calc(100%-4.5rem)]">
          <ArtBarChart
            height="22rem"
            :loading="sectionLoading.trend"
            :data="trendChartSeries"
            :x-axis-data="trendChartXAxisData"
            :show-axis-line="false"
            :show-legend="true"
            :show-split-line="true"
            legend-position="top"
            bar-width="38%"
          />
        </div>
      </div>
      <div class="art-card h-115 overflow-hidden p-6 box-border" v-loading="sectionLoading.locUsage">
        <div class="art-card-header">
          <div class="title">
            <h4>库位使用分布</h4>
            <p>{{ usageLegendCount }} 个维度</p>
        <div class="art-card box-border flex h-full min-h-0 flex-col overflow-hidden p-4 lg:p-5" v-loading="sectionLoading.locUsage">
          <div class="art-card-header">
            <div class="title">
              <h4>库位使用分布</h4>
              <p>{{ usageLegendCount }} 个维度</p>
            </div>
          </div>
        </div>
        <div class="grid h-[calc(100%-4.5rem)] gap-6 lg:grid-cols-[1fr_0.95fr] lg:items-center">
          <ArtRingChart
            height="21rem"
            :data="locUsageList"
            center-text="库位占比"
            :show-legend="false"
            :show-label="false"
          />
          <div class="space-y-1">
            <div
              v-for="item in usageLegend"
              :key="item.name"
              class="flex items-center justify-between border-b border-[var(--art-border-color)] py-4 last:border-b-0"
            >
              <div class="flex items-center gap-3">
                <span class="size-2.5 rounded-full" :style="{ backgroundColor: item.color }"></span>
                <span class="text-sm text-[var(--art-gray-900)]">{{ item.name }}</span>
              </div>
              <span class="text-sm font-medium text-[var(--art-gray-700)]">{{ item.value }}%</span>
          <div class="mt-3 grid min-h-0 flex-1 gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(220px,0.92fr)] lg:items-center">
            <div class="min-h-0 h-full">
              <ArtRingChart
                height="100%"
                :data="locUsageList"
                center-text="库位占比"
                :show-legend="false"
                :show-label="false"
              />
            </div>
            <ElEmpty v-if="!locUsageList.length" description="暂无库位使用数据" :image-size="88" />
          </div>
        </div>
      </div>
    </section>
    <section class="grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
      <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.tasks">
        <div class="art-card-header">
          <div class="title">
            <h4>执行中任务</h4>
            <p>{{ taskSubtitle }}</p>
          </div>
        </div>
        <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden">
          <ElScrollbar>
            <div
              v-for="item in taskCardItems"
              :key="`${item.time}-${item.title}`"
              class="flex gap-4 border-b border-g-300 py-4 last:border-b-0"
            >
              <div class="flex flex-col items-center pt-1">
                <span class="size-3 rounded-full bg-[var(--el-color-primary)]"></span>
                <span class="mt-2 min-h-10 w-px bg-[var(--art-border-color)]"></span>
              </div>
              <div class="min-w-0 flex-1">
                <p class="text-xs text-g-500">{{ item.time }}</p>
                <div class="mt-2 flex items-center gap-2">
                  <p class="truncate text-base font-medium text-[var(--art-gray-900)]">
                    {{ item.title }}
                  </p>
                  <ElTag size="small" effect="light" :type="item.tagType">{{ item.tagText }}</ElTag>
            <div class="min-h-0 overflow-hidden">
              <div
                v-for="item in usageLegend"
                :key="item.name"
                class="flex items-center justify-between gap-3 border-b border-[var(--art-border-color)] py-3 last:border-b-0"
              >
                <div class="flex items-center gap-3">
                  <span class="size-2.5 rounded-full" :style="{ backgroundColor: item.color }"></span>
                  <span class="truncate text-sm text-[var(--art-gray-900)]">{{ item.name }}</span>
                </div>
                <p class="mt-2 text-sm text-g-600">{{ item.subtitle }}</p>
                <span class="text-sm font-medium text-[var(--art-gray-700)]">{{ item.value }}%</span>
              </div>
              <ElEmpty v-if="!locUsageList.length" description="暂无库位使用数据" :image-size="88" />
            </div>
          </ElScrollbar>
          </div>
        </div>
      </div>
      <div class="art-card h-98 p-5 box-border" v-loading="sectionLoading.deadStock">
      <div class="art-card box-border flex shrink-0 flex-col p-4 lg:p-3.5">
        <div class="art-card-header">
          <div class="title">
            <h4>库存最近动态</h4>
            <p>{{ deadStockSubtitle }}</p>
            <h4>快捷跳转</h4>
            <p>快速进入任务页和库存页</p>
          </div>
        </div>
        <div class="mt-3 h-[calc(100%-3.75rem)] overflow-hidden">
          <ElScrollbar>
            <div
              v-for="item in stockCardItems"
              :key="`${item.title}-${item.time}`"
              class="flex items-center gap-4 border-b border-g-300 py-4 last:border-b-0"
            >
              <div class="size-12 rounded-2xl bg-[var(--el-color-primary-light-9)] flex-cc">
                <ArtSvgIcon :icon="item.icon" class="text-xl text-[var(--el-color-primary)]" />
              </div>
              <div class="min-w-0 flex-1">
                <p class="truncate text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
                <p class="mt-1 text-sm text-g-500">{{ item.status }}</p>
              </div>
              <div class="max-w-40 text-right text-sm text-g-500">{{ item.time }}</div>
        <div class="mt-2.5 grid gap-2 md:grid-cols-2">
          <button
            v-for="item in quickLinkCards"
            :key="item.title"
            type="button"
            class="flex items-start justify-between rounded-3xl border border-[var(--art-border-color)] bg-[var(--art-main-bg-color)] px-4 py-3 text-left transition hover:border-[var(--el-color-primary-light-5)] hover:bg-[var(--el-color-primary-light-9)]"
            @click="navigateTo(item.path)"
          >
            <div class="min-w-0 pr-4">
              <p class="text-base font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
              <p class="mt-0.5 text-sm text-g-500">{{ item.description }}</p>
            </div>
          </ElScrollbar>
            <div class="flex size-11 shrink-0 items-center justify-center rounded-2xl" :class="item.iconBoxClass">
              <ArtSvgIcon :icon="item.icon" class="text-xl" :class="item.iconClass" />
            </div>
          </button>
        </div>
      </div>
    </section>
@@ -145,44 +116,55 @@
</template>
<script setup>
  import { useRouter } from 'vue-router'
  import { storeToRefs } from 'pinia'
  import { useUserStore } from '@/store/modules/user'
  import {
    fetchDashboardHeader,
    fetchDashboardTrend,
    fetchDashboardDeadStock,
    fetchDashboardLocUsage,
    fetchDashboardTasks
    fetchDashboardLocUsage
  } from '@/api/dashboard'
  import {
    EMPTY_SUMMARY,
    buildDashboardDeadStockQuery,
    buildDashboardTaskQuery,
    normalizeDashboardSummary,
    normalizeDashboardTrend,
    normalizeDashboardTaskList,
    normalizeDashboardLocUsage,
    normalizeDashboardDeadStockList,
    withDashboardRequestGuard
  } from './consolePage.helpers'
  defineOptions({ name: 'Console' })
  const router = useRouter()
  const summary = ref({ ...EMPTY_SUMMARY })
  const trendModel = ref(normalizeDashboardTrend())
  const taskList = ref([])
  const deadStockList = ref([])
  const locUsageList = ref([])
  const sectionLoading = reactive({
    summary: false,
    trend: false,
    deadStock: false,
    locUsage: false,
    tasks: false
    locUsage: false
  })
  const userStore = useUserStore()
  const { getUserInfo } = storeToRefs(userStore)
  const quickLinkCards = [
    {
      title: '任务页',
      description: '查看和处理任务管理数据',
      path: '/manager/task',
      icon: 'ri:task-line',
      iconBoxClass: 'bg-[var(--el-color-primary-light-9)]',
      iconClass: 'text-[var(--el-color-primary)]'
    },
    {
      title: '库存页',
      description: '查看当前库存与库存明细',
      path: '/manager/stock',
      icon: 'ri:archive-stack-line',
      iconBoxClass: 'bg-[rgba(20,222,186,0.14)]',
      iconClass: 'text-[#14DEBA]'
    }
  ]
  const currentUser = computed(() => getUserInfo.value || {})
  const currentUserName = computed(() => {
@@ -291,8 +273,6 @@
  })
  const trendChartXAxisData = computed(() => trendDisplayModel.value.xAxisData)
  const trendChartSeries = computed(() => trendDisplayModel.value.series)
  const taskSubtitle = computed(() => `最近 ${taskList.value.length} 条任务动态`)
  const deadStockSubtitle = computed(() => `最近 ${deadStockList.value.length} 条库存记录`)
  const usageLegendCount = computed(() => locUsageList.value.length)
  const usageLegend = computed(() => {
    const palette = ['#5B8FF9', '#5AD8A6', '#5D7092', '#F6BD16', '#E8684A', '#6DC8EC']
@@ -301,16 +281,6 @@
      color: palette[index % palette.length]
    }))
  })
  const taskCardItems = computed(() =>
    taskList.value.slice(0, 6).map((item) => ({
      title: item.title,
      time: item.time,
      subtitle: item.status,
      tagText: resolveTaskTagText(item.status),
      tagType: resolveTaskTagType(item.class)
    }))
  )
  const stockCardItems = computed(() => deadStockList.value.slice(0, 6))
  onMounted(() => {
    loadDashboard()
@@ -319,9 +289,7 @@
  function loadDashboard() {
    void loadSummarySection()
    void loadTrendSection()
    void loadDeadStockSection()
    void loadLocUsageSection()
    void loadTaskSection()
  }
  async function loadSummarySection() {
@@ -338,16 +306,6 @@
    sectionLoading.trend = false
  }
  async function loadDeadStockSection() {
    sectionLoading.deadStock = true
    const payload = await withDashboardRequestGuard(
      fetchDashboardDeadStock(buildDashboardDeadStockQuery()),
      {}
    )
    deadStockList.value = normalizeDashboardDeadStockList(payload || {})
    sectionLoading.deadStock = false
  }
  async function loadLocUsageSection() {
    sectionLoading.locUsage = true
    const payload = await withDashboardRequestGuard(fetchDashboardLocUsage(), null)
@@ -355,29 +313,8 @@
    sectionLoading.locUsage = false
  }
  async function loadTaskSection() {
    sectionLoading.tasks = true
    const payload = await withDashboardRequestGuard(
      fetchDashboardTasks(buildDashboardTaskQuery()),
      {}
    )
    taskList.value = normalizeDashboardTaskList(payload || {})
    sectionLoading.tasks = false
  }
  function resolveTaskTagType(statusClass) {
    const text = String(statusClass || '')
    if (text.includes('emerald')) return 'success'
    if (text.includes('rose')) return 'danger'
    return 'primary'
  }
  function resolveTaskTagText(statusText) {
    const text = String(statusText || '')
      .replace(/\b\d+\./g, '')
      .replace(/\s+/g, ' ')
      .trim()
    const parts = text.split('·').map((item) => item.trim()).filter(Boolean)
    return parts[parts.length - 1] || '处理中'
  function navigateTo(path) {
    if (!path) return
    router.push(path)
  }
</script>
rsf-design/src/views/orders/asn-order-log/asnOrderLogPage.helpers.js
@@ -6,6 +6,7 @@
  density: 'compact',
  showSequence: true
}
export const DEFAULT_ASN_ORDER_LOG_PAGE_SIZE = 20
const RLE_STATUS_META = {
  0: { text: '正常', type: 'info' },
@@ -33,6 +34,14 @@
  }
  const parsed = Number(value)
  return Number.isNaN(parsed) ? fallback : parsed
}
function normalizePositiveInteger(value, fallback) {
  const parsed = normalizeNumber(value, fallback)
  if (!Number.isInteger(parsed) || parsed <= 0) {
    return fallback
  }
  return parsed
}
function normalizeDateText(value) {
@@ -119,8 +128,13 @@
export function buildAsnOrderLogPageQueryParams(params = {}) {
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    pageSize: normalizePositiveInteger(
      params.pageSize ?? params.size,
      DEFAULT_ASN_ORDER_LOG_PAGE_SIZE
    ),
    ...(params.cursor !== undefined && params.cursor !== null && params.cursor !== ''
      ? { cursor: normalizeNumber(params.cursor) }
      : {}),
    ...buildAsnOrderLogSearchParams(params)
  }
}
@@ -129,7 +143,7 @@
  return {
    logId: params.logId,
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20
    pageSize: params.pageSize || params.size || DEFAULT_ASN_ORDER_LOG_PAGE_SIZE
  }
}
rsf-design/src/views/orders/asn-order-log/index.vue
@@ -34,6 +34,7 @@
        :data="data"
        :columns="columns"
        :pagination="pagination"
        :pagination-options="mainPaginationOptions"
        @selection-change="handleSelectionChange"
        @pagination:size-change="handleSizeChange"
        @pagination:current-change="handleCurrentChange"
@@ -59,20 +60,20 @@
  import { computed, onMounted, reactive, ref } from 'vue'
  import { ElMessage } from 'element-plus'
  import { useUserStore } from '@/store/modules/user'
  import { useTable } from '@/hooks/core/useTable'
  import { useTableColumns } from '@/hooks/core/useTableColumns'
  import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { fetchDictDataPage } from '@/api/system-manage'
  import {
    DEFAULT_ASN_ORDER_LOG_PAGE_SIZE,
    buildAsnOrderLogDetailQueryParams,
    buildAsnOrderLogPageQueryParams,
    buildAsnOrderLogPrintRows,
    buildAsnOrderLogReportMeta,
    buildAsnOrderLogSearchParams,
    createAsnOrderLogSearchState,
    getAsnOrderLogExceStatusOptions,
    getAsnOrderLogNtyStatusOptions,
    getAsnOrderLogRleStatusOptions,
    getAsnOrderLogStatusOptions,
@@ -101,6 +102,8 @@
  const reportTitle = ASN_ORDER_LOG_REPORT_TITLE
  const searchForm = ref(createAsnOrderLogSearchState())
  const selectedRows = ref([])
  const data = ref([])
  const loading = ref(false)
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const detailItemsLoading = ref(false)
@@ -111,6 +114,10 @@
  const wkTypeOptions = ref([])
  const exceStatusOptions = ref([])
  const detailItemColumns = createAsnOrderItemLogColumns()
  const pageSize = ref(DEFAULT_ASN_ORDER_LOG_PAGE_SIZE)
  const cursorHistory = ref([null])
  const nextCursor = ref(null)
  const hasNext = ref(false)
  const detailPagination = reactive({
    current: 1,
@@ -119,6 +126,22 @@
  })
  const reportQueryParams = computed(() => buildAsnOrderLogSearchParams(searchForm.value))
  const mainPaginationOptions = {
    layout: 'prev, next, sizes',
    hideOnSinglePage: false
  }
  const pagination = computed(() => {
    const current = Math.max(1, cursorHistory.value.length || 1)
    const size = Number(pageSize.value) > 0 ? Number(pageSize.value) : DEFAULT_ASN_ORDER_LOG_PAGE_SIZE
    const recordCount = Math.max(0, Number(data.value.length) || 0)
    const total = hasNext.value ? current * size + 1 : (current - 1) * size + recordCount
    return {
      current,
      size,
      total
    }
  })
  const searchItems = computed(() => [
    {
@@ -236,10 +259,48 @@
    }
  ])
  function updatePaginationState(target, response, fallbackCurrent, fallbackSize) {
    target.total = Number(response?.total || 0)
    target.current = Number(response?.current || fallbackCurrent || 1)
    target.size = Number(response?.size || fallbackSize || target.size || 20)
  const { columns, columnChecks } = useTableColumns(() =>
    createAsnOrderLogTableColumns({ handleView: openDetail })
  )
  async function loadMainList({ history = cursorHistory.value } = {}) {
    loading.value = true
    try {
      const normalizedHistory =
        Array.isArray(history) && history.length > 0 ? [...history] : [null]
      const response = await guardRequestWithMessage(
        fetchAsnOrderLogPage(
          buildAsnOrderLogPageQueryParams({
            ...searchForm.value,
            cursor: normalizedHistory[normalizedHistory.length - 1] ?? null,
            pageSize: pageSize.value
          })
        ),
        {
          records: [],
          pageSize: pageSize.value,
          nextCursor: null,
          hasNext: false
        },
        {
          timeoutMessage: '历史通知单列表加载超时,已停止等待'
        }
      )
      cursorHistory.value = normalizedHistory
      data.value = Array.isArray(response?.records)
        ? response.records.map((item) => normalizeAsnOrderLogRow(item))
        : []
      nextCursor.value = response?.nextCursor ?? null
      hasNext.value = Boolean(
        response?.hasNext && response?.nextCursor !== null && response?.nextCursor !== undefined
      )
      selectedRows.value = []
    } catch (error) {
      ElMessage.error(error?.message || '获取历史通知单列表失败')
    } finally {
      loading.value = false
    }
  }
  function openDetail(row) {
@@ -248,30 +309,6 @@
    detailDrawerVisible.value = true
    loadDetailResources()
  }
  const {
    columns,
    columnChecks,
    data,
    loading,
    pagination,
    replaceSearchParams,
    resetSearchParams,
    handleSizeChange,
    handleCurrentChange,
    refreshData,
    getData
  } = useTable({
    core: {
      apiFn: fetchAsnOrderLogPage,
      apiParams: buildAsnOrderLogPageQueryParams(searchForm.value),
      columnsFactory: () => createAsnOrderLogTableColumns({ handleView: openDetail })
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeAsnOrderLogRow(item)) : []
    }
  })
  async function loadDetailResources() {
    if (!activeLogId.value) {
@@ -309,12 +346,9 @@
      detailItemRows.value = Array.isArray(itemResult.records)
        ? itemResult.records.map((item) => normalizeAsnOrderItemLogRow(item))
        : []
      updatePaginationState(
        detailPagination,
        itemResult,
        detailPagination.current,
        detailPagination.size
      )
      detailPagination.total = Number(itemResult?.total || 0)
      detailPagination.current = Number(itemResult?.current || detailPagination.current || 1)
      detailPagination.size = Number(itemResult?.size || detailPagination.size || 20)
    } catch (error) {
      detailDrawerVisible.value = false
      detailData.value = {}
@@ -330,18 +364,56 @@
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  function handleSearch(params) {
  async function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params
    }
    replaceSearchParams(buildAsnOrderLogSearchParams(searchForm.value))
    getData()
    await loadMainList({ history: [null] })
  }
  function handleReset() {
  async function handleReset() {
    searchForm.value = createAsnOrderLogSearchState()
    resetSearchParams()
    await loadMainList({ history: [null] })
  }
  async function handleSizeChange(size) {
    if (Number(size) <= 0) {
      return
    }
    pageSize.value = Number(size)
    await loadMainList({ history: [null] })
  }
  async function handleCurrentChange(current) {
    const targetPage = Number(current)
    const currentPage = Number(pagination.value.current)
    if (!Number.isInteger(targetPage) || targetPage <= 0 || targetPage === currentPage) {
      return
    }
    if (loading.value) {
      return
    }
    if (targetPage === currentPage + 1) {
      if (!hasNext.value || nextCursor.value === null || nextCursor.value === undefined) {
        return
      }
      const nextHistory = [...cursorHistory.value, Number(nextCursor.value)]
      await loadMainList({ history: nextHistory })
      return
    }
    if (targetPage === currentPage - 1) {
      await loadMainList({
        history: cursorHistory.value.length > 1 ? cursorHistory.value.slice(0, -1) : [null]
      })
    }
  }
  async function refreshData() {
    await loadMainList()
  }
  function handleDetailSizeChange(size) {
@@ -368,18 +440,41 @@
    }
  }
  async function fetchAllPrintableRecords(queryParams = {}, maxResults = 1000) {
    const records = []
    let cursor = null
    const batchSize = Math.max(Number(pageSize.value) || DEFAULT_ASN_ORDER_LOG_PAGE_SIZE, 100)
    while (records.length < maxResults) {
      const response = await fetchAsnOrderLogPage(
        buildAsnOrderLogPageQueryParams({
          ...queryParams,
          cursor,
          pageSize: batchSize
        })
      )
      const pageRecords = Array.isArray(response?.records) ? response.records : []
      if (pageRecords.length === 0) {
        break
      }
      records.push(...pageRecords)
      if (!response?.hasNext || response?.nextCursor === null || response?.nextCursor === undefined) {
        break
      }
      cursor = response.nextCursor
    }
    return records.slice(0, maxResults)
  }
  async function resolvePrintRecords(payload) {
    if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
      return defaultResponseAdapter(await fetchGetAsnOrderLogMany(payload.ids)).records
    }
    return defaultResponseAdapter(
      await fetchAsnOrderLogPage({
        ...reportQueryParams.value,
        current: 1,
        pageSize:
          Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
      })
    ).records
    return fetchAllPrintableRecords(reportQueryParams.value)
  }
  const {
@@ -463,6 +558,11 @@
  }
  onMounted(async () => {
    await Promise.allSettled([loadTypeOptions(), loadWkTypeOptions(), loadExceStatusOptions()])
    await Promise.allSettled([
      loadTypeOptions(),
      loadWkTypeOptions(),
      loadExceStatusOptions(),
      loadMainList()
    ])
  })
</script>
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/CursorPageParam.java
New file
@@ -0,0 +1,68 @@
package com.vincent.rsf.server.common.domain;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
/**
 * 通用游标分页参数。
 *
 * <p>这个类在现有 {@link BaseParam} 的基础上只增加一个 {@code cursor} 字段,
 * 用来表示“下一页从哪里继续查”。</p>
 *
 * <p>当前项目里的普通分页接口大多还是 {@code current + pageSize} 模式,
 * 但游标分页只依赖:</p>
 * <ul>
 *     <li>{@code pageSize}:每页大小</li>
 *     <li>{@code cursor}:上一页最后一条记录的游标值</li>
 * </ul>
 *
 * <p>这里仍然继承 {@link BaseParam},目的是继续复用原有的:</p>
 * <ul>
 *     <li>筛选条件解析</li>
 *     <li>{@code condition / timeStart / timeEnd} 等通用参数</li>
 *     <li>和 {@link com.vincent.rsf.server.common.domain.PageParam} 的协作能力</li>
 * </ul>
 *
 * <p>注意:前端即使继续传 {@code current},这里也不会报错;
 * {@link BaseParam#syncMap(Map)} 会正常解析它,但后续通用游标分页逻辑不会使用它。</p>
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class CursorPageParam extends BaseParam {
    /**
     * 当前页的起始游标。
     *
     * <p>约定含义是:只查询“比这个游标更旧”的数据,
     * 例如按 {@code id desc} 翻页时,会生成 {@code id < cursor} 的条件。</p>
     */
    private Long cursor;
    @Override
    public void syncMap(Map<String, Object> map) {
        // 先复用 BaseParam 的通用参数解析能力,
        // 保证 pageSize、condition、timeStart、timeEnd 等字段照常工作。
        super.syncMap(map);
        if (map == null) {
            return;
        }
        Object cursorValue = map.get("cursor");
        if (cursorValue == null) {
            return;
        }
        String normalizedValue = String.valueOf(cursorValue).trim();
        if (normalizedValue.isEmpty()) {
            // 空字符串游标等价于“首屏没有游标”,这里直接移除,避免后续误参与 SQL。
            map.remove("cursor");
            return;
        }
        // 目前通用方案统一把游标约束为 Long,
        // 这样可以覆盖当前按主键/数字字段倒序翻页的业务场景。
        this.cursor = Long.parseLong(normalizedValue);
        // 解析完成后要从 map 中移除,避免 PageParam.buildWrapper(true)
        // 把 cursor 当成普通筛选字段再次拼进 where 条件。
        map.remove("cursor");
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/common/domain/CursorPageResult.java
New file
@@ -0,0 +1,43 @@
package com.vincent.rsf.server.common.domain;
import lombok.Data;
import java.util.List;
/**
 * 通用游标分页返回体。
 *
 * <p>对外响应结构刻意保持简单,只暴露游标分页真正需要的 4 个字段,
 * 不再返回传统页码分页里的 {@code total/current/pages}。</p>
 *
 * <p>字段语义约定:</p>
 * <ul>
 *     <li>{@code records}:当前页实际返回的数据</li>
 *     <li>{@code pageSize}:本次查询最终采用的分页大小(包含默认值回退后的结果)</li>
 *     <li>{@code nextCursor}:下一页应当使用的游标;没有下一页时为 {@code null}</li>
 *     <li>{@code hasNext}:是否还有下一页</li>
 * </ul>
 *
 * <p>这样设计的目的是让 controller 直接 {@code R.ok().add(result)},
 * 同时让前端只关心“有没有下一页”和“下一页该带什么 cursor”。</p>
 */
@Data
public class CursorPageResult<T> {
    /** 当前页数据列表。 */
    private List<T> records;
    /** 当前请求最终生效的分页大小。 */
    private Integer pageSize;
    /**
     * 下一页要携带的游标值。
     *
     * <p>约定取“当前页最后一条记录”的游标字段值。
     * 如果没有下一页,则返回 null。</p>
     */
    private Long nextCursor;
    /** 是否还存在下一页。 */
    private Boolean hasNext;
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/controller/AsnOrderLogController.java
@@ -8,11 +8,11 @@
import com.vincent.rsf.framework.common.R;
import com.vincent.rsf.server.common.utils.ExcelUtil;
import com.vincent.rsf.server.common.annotation.OperationLog;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.CursorPageParam;
import com.vincent.rsf.server.common.domain.KeyValVo;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.manager.entity.AsnOrderLog;
import com.vincent.rsf.server.manager.service.AsnOrderLogService;
import com.vincent.rsf.server.manager.utils.buildPageRowsUtils;
import com.vincent.rsf.server.system.controller.BaseController;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
@@ -25,15 +25,35 @@
@RestController
public class AsnOrderLogController extends BaseController {
    private static final int DEFAULT_CURSOR_PAGE_SIZE = 20;
    @Autowired
    private AsnOrderLogService asnOrderLogService;
    @PreAuthorize("hasAuthority('manager:asnOrderLog:list')")
    @PostMapping("/asnOrderLog/page")
    public R page(@RequestBody Map<String, Object> map) {
        BaseParam baseParam = buildParam(map, BaseParam.class);
        PageParam<AsnOrderLog, BaseParam> pageParam = new PageParam<>(baseParam, AsnOrderLog.class);
        return R.ok().add(asnOrderLogService.page(pageParam, pageParam.buildWrapper(true)));
        // 这里已经不再手写游标分页细节,而是直接复用 BaseController 的通用实现。
        //
        // 当前这几个参数分别表达:
        // 1. map:前端原始请求参数
        // 2. CursorPageParam.class:通用游标参数解析器
        // 3. AsnOrderLog.class:用于通用筛选和 condition 模糊搜索
        // 4. asnOrderLogService:实际执行查询的 service
        // 5. "id":本接口的游标字段,固定按 id desc 做分页
        // 6. DEFAULT_CURSOR_PAGE_SIZE:默认每页大小
        // 7. null:当前没有额外的 where 条件扩展
        // 8. buildPageRowsUtils::userNameMap:结果后处理,批量补 createBy$/updateBy$
        return R.ok().add(cursorPage(
                map,
                CursorPageParam.class,
                AsnOrderLog.class,
                asnOrderLogService,
                "id",
                DEFAULT_CURSOR_PAGE_SIZE,
                null,
                buildPageRowsUtils::userNameMap
        ));
    }
    @PreAuthorize("hasAuthority('manager:asnOrderLog:list')")
rsf-server/src/main/java/com/vincent/rsf/server/manager/entity/AsnOrderLog.java
@@ -3,22 +3,19 @@
import java.text.SimpleDateFormat;
import java.util.*;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.vincent.rsf.server.system.constant.DictTypeCode;
import com.vincent.rsf.server.system.entity.DictData;
import com.vincent.rsf.server.system.service.DictDataService;
import org.springframework.format.annotation.DateTimeFormat;
import com.baomidou.mybatisplus.annotation.TableLogic;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@@ -141,6 +138,9 @@
    @ApiModelProperty(value= "添加人员")
    private Long createBy;
    @TableField(exist = false)
    private String createBy$;
    /**
     * 添加时间
     */
@@ -154,6 +154,9 @@
     */
    @ApiModelProperty(value= "修改人员")
    private Long updateBy;
    @TableField(exist = false)
    private String updateBy$;
    /**
     * 修改时间
@@ -296,29 +299,11 @@
        }
    }
    public String getCreateBy$(){
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.createBy);
        if (!Cools.isEmpty(user)){
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getCreateTime$(){
        if (Cools.isEmpty(this.createTime)){
            return "";
        }
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(this.createTime);
    }
    public String getUpdateBy$(){
        UserService service = SpringUtils.getBean(UserService.class);
        User user = service.getById(this.updateBy);
        if (!Cools.isEmpty(user)){
            return String.valueOf(user.getNickname());
        }
        return null;
    }
    public String getUpdateTime$(){
rsf-server/src/main/java/com/vincent/rsf/server/manager/mapper/AsnOrderMapper.java
@@ -1,14 +1,9 @@
package com.vincent.rsf.server.manager.mapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.vincent.rsf.server.manager.controller.dto.DashboardDto;
import com.vincent.rsf.server.manager.controller.dto.StockTransItemDto;
import com.vincent.rsf.server.manager.entity.StockStatistic;
import com.vincent.rsf.server.manager.entity.WkOrder;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.List;
@@ -17,7 +12,5 @@
@Repository
public interface AsnOrderMapper extends BaseMapper<WkOrder> {
    DashboardDto getDashbord(@Param("type") String type, @Param("taskType") String taskType);
    List<StockTransItemDto> getStockTrand(@Param(Constants.WRAPPER) LambdaQueryWrapper<StockStatistic> queryWrapper);
    List<StockTransItemDto> getStockTrand();
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/schedules/AsnOrderPressureSchedules.java
@@ -2,11 +2,11 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.manager.entity.AsnOrderItemLog;
import com.vincent.rsf.server.manager.entity.AsnOrderLog;
import com.vincent.rsf.server.manager.entity.Matnr;
import com.vincent.rsf.server.manager.entity.WkOrder;
import com.vincent.rsf.server.manager.entity.WkOrderItem;
import com.vincent.rsf.server.manager.service.AsnOrderItemService;
import com.vincent.rsf.server.manager.service.AsnOrderService;
import com.vincent.rsf.server.manager.service.AsnOrderItemLogService;
import com.vincent.rsf.server.manager.service.AsnOrderLogService;
import com.vincent.rsf.server.manager.service.MatnrService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -22,7 +22,9 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
/**
@@ -41,9 +43,9 @@
    private final AtomicBoolean running = new AtomicBoolean(false);
    @Autowired
    private AsnOrderService asnOrderService;
    private AsnOrderLogService asnOrderLogService;
    @Autowired
    private AsnOrderItemService asnOrderItemService;
    private AsnOrderItemLogService asnOrderItemLogService;
    @Autowired
    private MatnrService matnrService;
@@ -87,23 +89,33 @@
            LocalDateTime nowTime = LocalDateTime.now();
            double totalQty = itemCountPerOrder * itemQty;
            List<WkOrder> orders = new ArrayList<>(orderCountPerRun);
            List<AsnOrderLog> orders = new ArrayList<>(orderCountPerRun);
            for (int i = 0; i < orderCountPerRun; i++) {
                orders.add(buildOrder(now, nowTime, totalQty, i));
            }
            if (!asnOrderService.saveBatch(orders, 200)) {
            if (!asnOrderLogService.saveBatch(orders, 200)) {
                throw new CoolException("ASN压测主单插入失败");
            }
            List<WkOrderItem> items = new ArrayList<>(orderCountPerRun * itemCountPerOrder);
            List<AsnOrderLog> persistedOrders = asnOrderLogService.list(new LambdaQueryWrapper<AsnOrderLog>()
                    .in(AsnOrderLog::getCode, extractOrderCodes(orders)));
            Map<String, AsnOrderLog> orderMap = new HashMap<>(persistedOrders.size());
            for (AsnOrderLog order : persistedOrders) {
                orderMap.put(order.getCode(), order);
            }
            List<AsnOrderItemLog> items = new ArrayList<>(orderCountPerRun * itemCountPerOrder);
            for (int orderIndex = 0; orderIndex < orders.size(); orderIndex++) {
                WkOrder order = orders.get(orderIndex);
                AsnOrderLog order = orderMap.get(orders.get(orderIndex).getCode());
                if (order == null) {
                    throw new CoolException("ASN压测主单回查失败");
                }
                for (int itemIndex = 0; itemIndex < itemCountPerOrder; itemIndex++) {
                    Matnr matnr = matnrs.get((orderIndex * itemCountPerOrder + itemIndex) % matnrs.size());
                    items.add(buildOrderItem(order, matnr, now, orderIndex, itemIndex));
                }
            }
            if (!asnOrderItemService.saveBatch(items, 500)) {
            if (!asnOrderItemLogService.saveBatch(items, 500)) {
                throw new CoolException("ASN压测明细插入失败");
            }
@@ -126,89 +138,69 @@
                .last("limit " + needCount));
    }
    private WkOrder buildOrder(Date now, LocalDateTime nowTime, double totalQty, int sequence) {
    private List<String> extractOrderCodes(List<AsnOrderLog> orders) {
        List<String> codes = new ArrayList<>(orders.size());
        for (AsnOrderLog order : orders) {
            codes.add(order.getCode());
        }
        return codes;
    }
    private AsnOrderLog buildOrder(Date now, LocalDateTime nowTime, double totalQty, int sequence) {
        String suffix = String.format("%04d", sequence + 1);
        String code = "erp" + nowTime.format(ORDER_CODE_FORMATTER) + suffix;
        long serialNo = System.currentTimeMillis() * 1000 + sequence;
        return new WkOrder()
                .setCode(code)
                .setPoCode(code)
                .setPoId(serialNo)
                .setType(ORDER_TYPE)
                .setWkType(ORDER_WORK_TYPE)
                .setAnfme(totalQty)
                .setQty(totalQty)
                .setWorkQty(0.0)
                .setCheckType(0)
                .setRleStatus((short) 0)
                .setNtyStatus(0)
                .setExceStatus((short) 4)
                .setStatus(1)
                .setDeleted(0)
                .setTenantId(TENANT_ID)
                .setCreateBy(USER_ID)
                .setCreateTime(now)
                .setUpdateBy(USER_ID)
                .setUpdateTime(now)
                .setMemo(MEMO)
                .setReportOnce(4)
                .setBusinessTime(now)
                .setStationId("1215")
                .setOrderInternalCode(String.valueOf(serialNo))
                .setStockDirect("stockDirect")
                .setCustomerId("custom1")
                .setCustomerName("客户1")
                .setSupplierId("gongys1")
                .setSupplierName("供应商1")
                .setStockOrgId("stockYH")
                .setStockOrgName("浙江银湖箱包有限公司仓库")
                .setPurchaseOrgId("yhcaigou")
                .setPurchaseOrgName("浙江银湖箱包有限公司采购")
                .setPurchaseUserId("caigouyuan1")
                .setPurchaseUserName("采购员1")
                .setPrdOrgId("prdYH")
                .setPrdOrgName("浙江银湖箱包有限公司")
                .setSaleOrgId("sale1")
                .setSaleOrgName("生产组1")
                .setSaleUserId("shengchanyuan1")
                .setSaleUserName("生产员1")
                .setVersion(0);
        AsnOrderLog order = new AsnOrderLog();
        order.setCode(code);
        order.setPoCode(code);
        order.setPoId(serialNo);
        order.setType(ORDER_TYPE);
        order.setWkType(ORDER_WORK_TYPE);
        order.setAnfme(totalQty);
        order.setQty(totalQty);
        order.setRleStatus((short) 0);
        order.setNtyStatus((short) 0);
        order.setExceStatus((short) 4);
        order.setStatus(1);
        order.setDeleted(0);
        order.setTenantId(TENANT_ID);
        order.setCreateBy(USER_ID);
        order.setCreateTime(now);
        order.setUpdateBy(USER_ID);
        order.setUpdateTime(now);
        order.setMemo(MEMO);
        return order;
    }
    private WkOrderItem buildOrderItem(WkOrder order, Matnr matnr, Date now, int orderIndex, int itemIndex) {
    private AsnOrderItemLog buildOrderItem(AsnOrderLog order, Matnr matnr, Date now, int orderIndex, int itemIndex) {
        String stockUnit = StringUtils.firstNonBlank(matnr.getStockUnit(), matnr.getPurUnit(), matnr.getUnit(), matnr.getBaseUnit());
        String purUnit = StringUtils.firstNonBlank(matnr.getPurUnit(), matnr.getUnit(), matnr.getStockUnit(), matnr.getBaseUnit());
        String baseUnit = StringUtils.firstNonBlank(matnr.getBaseUnit(), matnr.getUnit(), matnr.getStockUnit(), matnr.getPurUnit());
        String batchCode = "B" + new SimpleDateFormat("yyyyMMddHHmmss").format(now)
                + String.format("%02d%02d", orderIndex + 1, itemIndex + 1);
        String trackCode = "T" + System.currentTimeMillis() + String.format("%02d%02d", orderIndex + 1, itemIndex + 1);
        return new WkOrderItem()
                .setOrderId(order.getId())
                .setOrderCode(order.getCode())
        return new AsnOrderItemLog()
                .setLogId(order.getId())
                .setAsnId(order.getAsnId())
                .setAsnCode(order.getCode())
                .setPlatItemId("M" + (itemIndex + 1))
                .setPoCode(order.getPoCode())
                .setFieldsIndex(matnr.getFieldsIndex())
                .setMatnrId(matnr.getId())
                .setMatnrCode(matnr.getCode())
                .setMaktx(matnr.getName())
                .setSpec(matnr.getSpec())
                .setModel(matnr.getModel())
                .setAnfme(itemQty)
                .setWorkQty(0.0)
                .setPurQty(itemQty)
                .setQty(itemQty)
                .setStockUnit(stockUnit)
                .setPurUnit(purUnit)
                .setBatch(batchCode)
                .setSplrBatch(batchCode)
                .setSplrCode("gongys1")
                .setSplrName("供应商1")
                .setTrackCode(trackCode)
                .setBarcode(trackCode)
                .setProdTime(new SimpleDateFormat("yyyy-MM-dd").format(now))
                .setNtyStatus(0)
                .setNtyStatus((short) 0)
                .setStatus(1)
                .setDeleted(0)
                .setTenantId(TENANT_ID)
@@ -216,11 +208,6 @@
                .setCreateTime(now)
                .setUpdateBy(USER_ID)
                .setUpdateTime(now)
                .setMemo(MEMO)
                .setBaseUnit(baseUnit)
                .setUseOrgId(matnr.getUseOrgId())
                .setUseOrgName(matnr.getUseOrgName())
                .setErpClsId(matnr.getErpClsId())
                .setPriceUnitId(baseUnit);
                .setMemo(MEMO);
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/manager/service/impl/AsnOrderServiceImpl.java
@@ -8,6 +8,7 @@
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.api.entity.dto.PoItemsDto;
import com.vincent.rsf.server.api.service.ReportMsgService;
import com.vincent.rsf.server.common.service.RedisService;
import com.vincent.rsf.server.common.utils.DateUtils;
import com.vincent.rsf.server.manager.controller.dto.DashboardDto;
import com.vincent.rsf.server.manager.controller.dto.StockTrandDto;
@@ -17,12 +18,14 @@
import com.vincent.rsf.server.manager.entity.*;
import com.vincent.rsf.server.manager.enums.*;
import com.vincent.rsf.server.manager.mapper.AsnOrderMapper;
import com.vincent.rsf.server.manager.mapper.TaskLogMapper;
import com.vincent.rsf.server.manager.mapper.TaskMapper;
import com.vincent.rsf.server.manager.service.*;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.vincent.rsf.server.system.constant.SerialRuleCode;
import com.vincent.rsf.server.system.mapper.SerialRuleMapper;
import com.vincent.rsf.server.system.utils.SerialRuleUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -30,6 +33,9 @@
import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.text.DateFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
@@ -43,8 +49,16 @@
 * @return
 * @time 2025/3/7 08:02
 */
@Slf4j
@Service("asnOrderService")
public class AsnOrderServiceImpl extends ServiceImpl<AsnOrderMapper, WkOrder> implements AsnOrderService {
    private static final String DASHBOARD_HEADER_CACHE_FLAG = "DASHBOARD_HEADER";
    private static final String DASHBOARD_HEADER_CACHE_FRESH_SUFFIX = "FRESH";
    private static final String DASHBOARD_HEADER_CACHE_STALE_SUFFIX = "STALE";
    private static final int DASHBOARD_HEADER_CACHE_FRESH_TTL_SECONDS = 300;
    private static final int DASHBOARD_HEADER_CACHE_STALE_TTL_SECONDS = 86400;
    private static final DateTimeFormatter DASHBOARD_CACHE_DATE_FORMATTER = DateTimeFormatter.BASIC_ISO_DATE;
    @Autowired
    private ReportMsgService reportMsgService;
@@ -65,6 +79,10 @@
    private PurchaseItemService purchaseItemService;
    @Autowired
    private TaskMapper taskMapper;
    @Autowired
    private TaskLogMapper taskLogMapper;
    @Autowired
    private RedisService redisService;
    @Override
    public boolean notifyInspect(List<WkOrder> orders) {
@@ -430,21 +448,29 @@
     */
    @Override
    public R getDashbord() {
        DashboardDto dto = new DashboardDto();
        //获取入库数量
        DashboardDto trandDto = this.baseMapper.getDashbord(OrderType.ORDER_IN.type, TaskType.TASK_TYPE_IN.type + "");
        dto.setInAnf(trandDto.getAnfme()).setTaskIn(trandDto.getRealAnfme()).setTotalIn(trandDto.getAnfme() + trandDto.getRealAnfme());
        //获取出库单数量
        DashboardDto outTrand = this.baseMapper.getDashbord(OrderType.ORDER_OUT.type, TaskType.TASK_TYPE_OUT.type + "");
        dto.setOutAnf(outTrand.getAnfme()).setTaskOut(outTrand.getRealAnfme()).setTotalOut(outTrand.getAnfme() + outTrand.getRealAnfme());
        //获取执行中任务数量
        List<Task> tasks = taskMapper.selectList(new LambdaQueryWrapper<>());
        if (!tasks.isEmpty()) {
            dto.setTaskQty(tasks.size());
        String freshCacheKey = buildDashboardCacheKey(DASHBOARD_HEADER_CACHE_FRESH_SUFFIX);
        DashboardDto freshSnapshot = getDashboardCache(freshCacheKey);
        if (freshSnapshot != null) {
            return R.ok().add(freshSnapshot);
        }
        return R.ok().add(dto);
        String staleCacheKey = buildDashboardCacheKey(DASHBOARD_HEADER_CACHE_STALE_SUFFIX);
        Exception dbException = null;
        try {
            DashboardDto snapshot = buildDashboardSnapshot();
            cacheDashboard(freshCacheKey, snapshot, DASHBOARD_HEADER_CACHE_FRESH_TTL_SECONDS);
            cacheDashboard(staleCacheKey, snapshot, DASHBOARD_HEADER_CACHE_STALE_TTL_SECONDS);
            return R.ok().add(snapshot);
        } catch (Exception ex) {
            dbException = ex;
            log.warn("Load dashboard snapshot from database failed, fallback to stale cache. message={}", ex.getMessage(), ex);
        }
        DashboardDto staleSnapshot = getDashboardCache(staleCacheKey);
        if (staleSnapshot != null) {
            return R.ok().add(staleSnapshot);
        }
        log.error("Load dashboard snapshot failed, returning empty snapshot.", dbException);
        return R.ok().add(emptyDashboardSnapshot());
    }
    /**
@@ -454,9 +480,7 @@
    @Override
    public R getStockTrand() {
        List<String> days = DateUtils.getLastMonthDays("yyyy-MM-dd");
        LambdaQueryWrapper<StockStatistic> queryWrapper = new LambdaQueryWrapper<StockStatistic>()
                .in(StockStatistic::getTaskType, Arrays.asList(TaskType.TASK_TYPE_IN.type, TaskType.TASK_TYPE_OUT.type));
       List<StockTransItemDto> items = this.baseMapper.getStockTrand(queryWrapper);
       List<StockTransItemDto> items = this.baseMapper.getStockTrand();
       if (items.isEmpty()) {
           return R.ok();
       }
@@ -542,4 +566,86 @@
            throw new CoolException("原单据删除失败!!");
        }
    }
    private DashboardDto buildDashboardSnapshot() {
        Date[] todayRange = buildTodayRange();
        Date todayStart = todayRange[0];
        Date tomorrowStart = todayRange[1];
        int inAnf = safeToInt(this.count(new LambdaQueryWrapper<WkOrder>()
                .eq(WkOrder::getType, OrderType.ORDER_IN.type)
                .ge(WkOrder::getCreateTime, todayStart)
                .lt(WkOrder::getCreateTime, tomorrowStart)));
        int outAnf = safeToInt(this.count(new LambdaQueryWrapper<WkOrder>()
                .eq(WkOrder::getType, OrderType.ORDER_OUT.type)
                .ge(WkOrder::getCreateTime, todayStart)
                .lt(WkOrder::getCreateTime, tomorrowStart)));
        int taskIn = safeToInt(taskLogMapper.selectCount(new LambdaQueryWrapper<TaskLog>()
                .eq(TaskLog::getTaskType, TaskType.TASK_TYPE_IN.type)
                .ge(TaskLog::getCreateTime, todayStart)
                .lt(TaskLog::getCreateTime, tomorrowStart)));
        int taskOut = safeToInt(taskLogMapper.selectCount(new LambdaQueryWrapper<TaskLog>()
                .eq(TaskLog::getTaskType, TaskType.TASK_TYPE_OUT.type)
                .ge(TaskLog::getCreateTime, todayStart)
                .lt(TaskLog::getCreateTime, tomorrowStart)));
        int taskQty = safeToInt(taskMapper.selectCount(new LambdaQueryWrapper<Task>()));
        return new DashboardDto()
                .setInAnf(inAnf)
                .setOutAnf(outAnf)
                .setTaskIn(taskIn)
                .setTaskOut(taskOut)
                .setTaskQty(taskQty)
                .setTotalIn(inAnf + taskIn)
                .setTotalOut(outAnf + taskOut);
    }
    private DashboardDto getDashboardCache(String cacheKey) {
        try {
            String cacheValue = redisService.getValue(DASHBOARD_HEADER_CACHE_FLAG, cacheKey);
            if (StringUtils.isBlank(cacheValue)) {
                return null;
            }
            return JSONObject.parseObject(cacheValue, DashboardDto.class);
        } catch (Exception ex) {
            log.warn("Read dashboard cache failed, key={}, message={}", cacheKey, ex.getMessage(), ex);
            return null;
        }
    }
    private void cacheDashboard(String cacheKey, DashboardDto dto, int ttlSeconds) {
        try {
            redisService.setValue(DASHBOARD_HEADER_CACHE_FLAG, cacheKey, JSONObject.toJSONString(dto), ttlSeconds);
        } catch (Exception ex) {
            log.warn("Write dashboard cache failed, key={}, message={}", cacheKey, ex.getMessage(), ex);
        }
    }
    private String buildDashboardCacheKey(String suffix) {
        String dateBucket = LocalDate.now().format(DASHBOARD_CACHE_DATE_FORMATTER);
        return dateBucket + "." + suffix;
    }
    private Date[] buildTodayRange() {
        ZoneId zoneId = ZoneId.systemDefault();
        LocalDate today = LocalDate.now(zoneId);
        Date todayStart = Date.from(today.atStartOfDay(zoneId).toInstant());
        Date tomorrowStart = Date.from(today.plusDays(1).atStartOfDay(zoneId).toInstant());
        return new Date[]{todayStart, tomorrowStart};
    }
    private DashboardDto emptyDashboardSnapshot() {
        return new DashboardDto()
                .setInAnf(0)
                .setOutAnf(0)
                .setTaskIn(0)
                .setTaskOut(0)
                .setTaskQty(0)
                .setTotalIn(0)
                .setTotalOut(0);
    }
    private int safeToInt(Long count) {
        return count == null ? 0 : count.intValue();
    }
}
rsf-server/src/main/java/com/vincent/rsf/server/system/controller/BaseController.java
@@ -1,16 +1,27 @@
package com.vincent.rsf.server.system.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.vincent.rsf.common.utils.Utils;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.domain.CursorPageParam;
import com.vincent.rsf.server.common.domain.CursorPageResult;
import com.vincent.rsf.server.common.domain.PageParam;
import com.vincent.rsf.server.system.entity.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
/**
 * Created by vincent on 1/30/2024
@@ -105,5 +116,189 @@
        return true;
    }
    /**
     * 通用游标分页实现。
     *
     * <p>这个方法的目标不是替代所有分页,而是把“单字段、倒序、向后翻页”的游标分页
     * 收敛成一套统一实现,避免各个 controller 重复写下面这些样板逻辑:</p>
     * <ul>
     *     <li>buildParam</li>
     *     <li>忽略前端传入的 orderBy</li>
     *     <li>buildWrapper(true) 构建通用筛选</li>
     *     <li>cursor 条件</li>
     *     <li>按固定字段倒序</li>
     *     <li>多查一条判断 hasNext</li>
     *     <li>截断结果并生成 nextCursor</li>
     * </ul>
     *
     * <p>适用前提:</p>
     * <ul>
     *     <li>游标字段是单一字段</li>
     *     <li>游标字段的值稳定、可比较,并且能映射成 Long</li>
     *     <li>分页方向固定为“按该字段倒序,向更小的值翻页”</li>
     * </ul>
     *
     * <p>参数说明:</p>
     * <ul>
     *     <li>{@code map}:原始请求参数</li>
     *     <li>{@code paramClass}:游标参数类型,通常传 {@link CursorPageParam}</li>
     *     <li>{@code entityClass}:实体类,用于 PageParam 条件构建和 condition 模糊查询</li>
     *     <li>{@code service}:MyBatis-Plus 的 IService,负责执行 page 查询</li>
     *     <li>{@code cursorField}:实体字段名,不是数据库列名,例如传 {@code id}</li>
     *     <li>{@code defaultPageSize}:当前接口的默认分页大小</li>
     *     <li>{@code wrapperConsumer}:可选的额外 where 条件扩展钩子</li>
     *     <li>{@code recordsConsumer}:可选的结果后处理钩子,例如补充 createBy$/updateBy$</li>
     * </ul>
     */
    protected <T, U extends CursorPageParam> CursorPageResult<T> cursorPage(
            Map<String, Object> map,
            Class<U> paramClass,
            Class<T> entityClass,
            IService<T> service,
            String cursorField,
            int defaultPageSize,
            Consumer<QueryWrapper<T>> wrapperConsumer,
            Consumer<List<T>> recordsConsumer
    ) {
        // 允许 controller 传 null,内部统一兜底成空 map,
        // 这样 buildParam 不需要每个调用方自己先判空。
        U baseParam = buildParam(map == null ? new HashMap<>() : map, paramClass);
        // 游标分页不允许客户端自定义排序,
        // 否则“上一页最后一条作为下一页游标”的前提会被破坏。
        baseParam.setOrderBy(null);
        // pageSize 允许从请求里带入,但非法值(null、0、负数)统一回退到接口默认值。
        int pageSize = resolveCursorPageSize(baseParam.getPageSize(), defaultPageSize);
        // controller 传的是实体字段名,例如 "id" / "poId",
        // 这里统一转成数据库列名并补反引号,避免每个业务自己手写 SQL 片段。
        String cursorColumn = resolveCursorColumn(cursorField);
        // 先复用系统现有的 PageParam + buildWrapper(true) 机制,
        // 保留原来的条件解析、时间范围、condition 模糊搜索等能力。
        PageParam<T, U> pageParam = new PageParam<>(baseParam, entityClass);
        QueryWrapper<T> wrapper = pageParam.buildWrapper(true);
        // 给业务预留额外 where 条件的扩展点;
        // 如果某个接口除了通用筛选外,还要拼接额外限制,可以在这里补。
        if (wrapperConsumer != null) {
            wrapperConsumer.accept(wrapper);
        }
        // 游标分页的核心条件:
        // 当前约定是“按 cursorField 倒序查看更旧的数据”,所以条件固定为 < cursor。
        if (baseParam.getCursor() != null) {
            wrapper.lt(cursorColumn, baseParam.getCursor());
        }
        // 强制按游标字段倒序排序,保证每一页的数据顺序稳定。
        wrapper.orderByDesc(cursorColumn);
        // 多查一条是游标分页判断 hasNext 的常见做法:
        // 实际要 20 条,就查 21 条;多出来那一条只用来判断是否还有下一页。
        Page<T> queryPage = new Page<>(1L, pageSize + 1L, false);
        List<T> records = service.page(queryPage, wrapper).getRecords();
        List<T> pageRecords = Cools.isEmpty(records) ? new ArrayList<>() : new ArrayList<>(records);
        // 如果查出来的数量大于 pageSize,说明至少还有下一页。
        boolean hasNext = pageRecords.size() > pageSize;
        if (hasNext) {
            // 只把真正需要返回给前端的 pageSize 条数据留在当前页。
            pageRecords = new ArrayList<>(pageRecords.subList(0, pageSize));
        }
        // 给业务侧一个“结果出库前处理”的机会。
        // 典型场景是批量补充用户名、字典文本、缓存字段等,
        // 这样公共分页逻辑不关心业务细节,但业务也不需要回到 controller 自己重写分页。
        if (recordsConsumer != null && !Cools.isEmpty(pageRecords)) {
            recordsConsumer.accept(pageRecords);
        }
        CursorPageResult<T> result = new CursorPageResult<>();
        result.setRecords(pageRecords);
        result.setPageSize(pageSize);
        result.setHasNext(hasNext);
        // nextCursor 只有在还有下一页时才有意义;
        // 约定取“当前页最后一条记录”的游标字段值。
        result.setNextCursor(hasNext ? extractCursorValue(pageRecords, cursorField) : null);
        return result;
    }
    /**
     * 统一解析当前接口实际使用的 pageSize。
     *
     * <p>只要前端没传、传了 0、或者传了负数,就回退到 controller 传入的默认值。</p>
     */
    private int resolveCursorPageSize(Integer pageSize, int defaultPageSize) {
        if (pageSize == null || pageSize <= 0) {
            return defaultPageSize;
        }
        return pageSize;
    }
    /**
     * 把实体字段名转换成数据库列名。
     *
     * <p>例如:</p>
     * <ul>
     *     <li>{@code id -> `id`}</li>
     *     <li>{@code poId -> `po_id`}</li>
     * </ul>
     *
     * <p>这样 controller 调用时只需要关心 Java 字段名,不需要自己拼 SQL。</p>
     */
    private String resolveCursorColumn(String cursorField) {
        return "`" + Utils.toSymbolCase(cursorField, '_') + "`";
    }
    /**
     * 从当前页最后一条记录中提取 nextCursor。
     *
     * <p>这里使用反射而不是额外定义接口,目的是降低接入成本:
     * 只要实体里存在同名字段,就能直接复用通用方法。</p>
     *
     * <p>支持的字段值类型:</p>
     * <ul>
     *     <li>{@link Long}</li>
     *     <li>其他 {@link Number}</li>
     *     <li>可转成 Long 的字符串</li>
     * </ul>
     *
     * <p>如果字段不存在、为空、或无法转成 Long,则返回 null。</p>
     */
    private <T> Long extractCursorValue(List<T> records, String cursorField) {
        if (Cools.isEmpty(records)) {
            return null;
        }
        T lastRecord = records.get(records.size() - 1);
        if (lastRecord == null || Cools.isEmpty(cursorField)) {
            return null;
        }
        Field field = Cools.getField(lastRecord.getClass(), cursorField);
        if (field == null) {
            return null;
        }
        boolean accessible = field.isAccessible();
        try {
            field.setAccessible(true);
            Object value = field.get(lastRecord);
            if (value instanceof Long) {
                return (Long) value;
            }
            if (value instanceof Number) {
                return ((Number) value).longValue();
            }
            if (value instanceof String && !((String) value).trim().isEmpty()) {
                return Long.parseLong(((String) value).trim());
            }
        } catch (IllegalAccessException | NumberFormatException ignored) {
            return null;
        } finally {
            field.setAccessible(accessible);
        }
        return null;
    }
}
rsf-server/src/main/resources/mapper/manager/AsnOrderMapper.xml
@@ -1,17 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.vincent.rsf.server.manager.mapper.AsnOrderMapper">
    <select id="getDashbord" resultType="com.vincent.rsf.server.manager.controller.dto.DashboardDto">
        SELECT
            ( SELECT COUNT( 1 ) FROM man_asn_order WHERE DATE(create_time) = CURRENT_DATE()  AND `type` = #{type} ) AS anfme,
            COUNT( id ) AS real_anfme
        FROM
            man_task_log
        WHERE
            DATE(create_time) = CURRENT_DATE()
          AND task_type = #{taskType}
    </select>
    <select id="getStockTrand" resultType="com.vincent.rsf.server.manager.controller.dto.StockTransItemDto">
        SELECT * FROM
            (
@@ -26,11 +15,12 @@
                FROM
                    view_stock_statistic
                WHERE
                    task_type IN (1, 101)
                  AND
                    `day_time` BETWEEN ( CURDATE() - INTERVAL 1 MONTH )
                        AND CURDATE()
                GROUP BY
                    `day_time`, task_type,id
            ) t
        ${ew.customSqlSegment}
    </select>
</mapper>